Skip to content

docs/runtime: warn on external rewrites, sanitize Content-Disposition#418

Open
Divkix wants to merge 7 commits intocloudflare:mainfrom
Divkix:docs/external-rewrite-warning
Open

docs/runtime: warn on external rewrites, sanitize Content-Disposition#418
Divkix wants to merge 7 commits intocloudflare:mainfrom
Divkix:docs/external-rewrite-warning

Conversation

@Divkix
Copy link
Contributor

@Divkix Divkix commented Mar 10, 2026

Summary

  • Strip credential headers (cookie, authorization, proxy-authorization, x-api-key) from outbound proxy requests to external origins
  • Emit startup warning when next.config contains rewrites to external origins, noting that credential headers are stripped
  • Sanitize contentDispositionType in image optimization and prod server to only allow "attachment" (default "inline")

Closes #414
Closes #430

Test plan

  • Unit test verifies credential headers are stripped from proxied requests
  • Unit test verifies warning is emitted for external rewrites
  • Typecheck, lint, format all pass
  • Manual: add an external rewrite to a fixture config and verify warning on startup

Add a startup warning when next.config contains rewrites targeting
external origins (e.g. https://...), alerting that credential headers
(cookie, authorization, x-api-key, proxy-authorization) are forwarded.

Sanitize contentDispositionType in image optimization and prod server
to only allow "attachment" — any other value defaults to "inline",
preventing header injection via arbitrary config values.

Closes cloudflare#414
Copilot AI review requested due to automatic review settings March 10, 2026 18:38
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 10, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@418

commit: ec9f2d8

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime safeguards and visibility around two security-sensitive config areas in vinext: external rewrites (reverse-proxy behavior) and Content-Disposition handling for image responses.

Changes:

  • Emit a startup/config-resolution console.warn when next.config rewrites include external destinations, listing affected rewrites and noting credential header forwarding.
  • Sanitize images.contentDispositionType at runtime to only allow "attachment" (otherwise defaulting to "inline") in both image optimization and the Node prod server.
  • Add unit tests for the external rewrite warning behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
tests/next-config.test.ts Adds new tests asserting warnings are emitted (or not) based on presence of external rewrite destinations.
packages/vinext/src/config/next-config.ts Imports isExternalUrl and warns when resolved rewrites include external destinations.
packages/vinext/src/server/image-optimization.ts Forces Content-Disposition to "attachment" only when explicitly configured; otherwise "inline".
packages/vinext/src/server/prod-server.ts Applies the same Content-Disposition sanitization for static image serving in both app/pages router servers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +615 to +625
// Should have warned about the external rewrite
const externalWarning = warn.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("1 external rewrite");
expect(externalWarning![0]).toContain("https://api.example.com/:path*");
expect(externalWarning![0]).toContain("Credential headers");

// The internal rewrite should not appear in the warning
expect(externalWarning![0]).not.toContain("/other");
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only asserts that the warning contains the destination URL, but the intended behavior (and issue acceptance criteria) is to list each external rewrite as source → destination. Consider also asserting that the warning string includes the rewrite source and the arrow/formatting so regressions in the listing don’t slip through.

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +449
console.warn(
`[vinext] Found ${externalRewrites.length} external rewrite(s) that proxy requests to third-party origins:\n` +
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message uses the "external rewrite(s)" pluralization pattern. Consider switching to proper singular/plural wording (e.g., "1 external rewrite" vs "2 external rewrites") to improve log readability and grep-ability.

Suggested change
console.warn(
`[vinext] Found ${externalRewrites.length} external rewrite(s) that proxy requests to third-party origins:\n` +
const count = externalRewrites.length;
const noun = count === 1 ? "external rewrite" : "external rewrites";
console.warn(
`[vinext] Found ${count} ${noun} that proxy requests to third-party origins:\n` +

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +455
// Warn about external rewrites that act as reverse proxies
{
const allRewrites = [...rewrites.beforeFiles, ...rewrites.afterFiles, ...rewrites.fallback];
const externalRewrites = allRewrites.filter((r) => isExternalUrl(r.destination));
if (externalRewrites.length > 0) {
const listing = externalRewrites.map((r) => ` ${r.source} → ${r.destination}`).join("\n");
console.warn(
`[vinext] Found ${externalRewrites.length} external rewrite(s) that proxy requests to third-party origins:\n` +
`${listing}\n` +
`Credential headers (cookie, authorization, x-api-key, proxy-authorization) are forwarded to the external origin. ` +
`If this is unintentional, consider proxying at the CDN level instead.`,
);
}
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #414’s proposed solution includes adding/adjusting documentation to note the security implications of external rewrites, but this PR only adds a runtime warning and header sanitization (no docs changes). If docs updates are still required for closing the issue, consider adding them in this PR or tracking them explicitly in a follow-up.

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +177
headers.set(
"Content-Disposition",
config?.contentDispositionType === "attachment" ? "attachment" : "inline",
);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change intentionally sanitizes contentDispositionType to only allow "attachment" (otherwise defaulting to "inline"), but there isn’t a unit test covering the invalid/unsafe input case. Consider adding a test that passes a non-"attachment" value (e.g., via as any) and asserts the response header falls back to "inline" so the sanitization guarantee is enforced.

Copilot uses AI. Check for mistakes.
…itization test

Use proper singular/plural in the external rewrite warning message
instead of "rewrite(s)". Add test assertions verifying the warning
includes the source pattern and arrow separator. Add a test confirming
that invalid contentDispositionType values default to "inline".
@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR adds two security-related improvements: a startup warning for external rewrites that forward credentials, and sanitization of contentDispositionType for image responses. Both are reasonable changes. I have a couple of suggestions and one substantive concern.

Content-Disposition sanitization — The allowlist approach (=== "attachment" instead of passthrough) is the right call. All three codepaths (image-optimization, app router prod server, pages router prod server) are consistently updated, and the worker entry delegates to handleImageOptimization so it's covered too. Good.

External rewrite warning — Clean implementation, good test coverage. The warning message accurately reflects the current behavior of proxyExternalRequest (which does forward cookie, authorization, etc.).

One concern: the warning lists specific credential headers (cookie, authorization, x-api-key, proxy-authorization) but proxyExternalRequest actually forwards all request headers except connection and x-middleware-*. This is slightly misleading — it implies only those four are forwarded when the reality is worse. See inline comment.

`[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to third-party origins:\n` +
`${listing}\n` +
`Credential headers (cookie, authorization, x-api-key, proxy-authorization) are forwarded to the external origin. ` +
`If this is unintentional, consider proxying at the CDN level instead.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning says "Credential headers (cookie, authorization, x-api-key, proxy-authorization) are forwarded" — but looking at proxyExternalRequest in config-matchers.ts, it actually forwards all request headers (only stripping connection and x-middleware-*). This is a broader exposure than the warning suggests.

Consider either:

  1. Rewording to say "All request headers (including cookies, authorization tokens, etc.) are forwarded to the external origin" — which is more accurate, or
  2. Actually stripping credential headers in proxyExternalRequest and then the warning can say they're stripped (which would be a more meaningful security improvement but is a larger change)

Option 1 is a minimal fix for accuracy:

Suggested change
`If this is unintentional, consider proxying at the CDN level instead.`,
`All request headers (including cookies, authorization tokens, and other credentials) are forwarded to the external origin. ` +

`${listing}\n` +
`Credential headers (cookie, authorization, x-api-key, proxy-authorization) are forwarded to the external origin. ` +
`If this is unintentional, consider proxying at the CDN level instead.`,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this suggests "proxying at the CDN level" as an alternative, which is reasonable advice for Cloudflare users. But for non-Cloudflare deployments, this might not be as actionable. Consider mentioning the more general alternative of using server-side fetch in an API route/route handler, which works everywhere.

(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("3 external rewrite");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this assertion checks for "3 external rewrite" which matches both "3 external rewrites" (correct) and "3 external rewrite " (hypothetical singular bug). Consider asserting the exact plural form:

Suggested change
expect(externalWarning![0]).toContain("3 external rewrite");
expect(externalWarning![0]).toContain("3 external rewrites");

}),
};
const response = await handleImageOptimization(request, handlers, undefined, {
contentDispositionType: "bogus" as "inline",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition — testing the sanitization boundary with an invalid value cast via as. This directly exercises the security fix.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 10, 2026

Review posted. Here's a summary of the feedback I left:

Key concern: The warning message in next-config.ts lists specific credential headers (cookie, authorization, x-api-key, proxy-authorization) but proxyExternalRequest actually forwards all request headers except connection and x-middleware-*. The warning understates the exposure. I suggested rewording to be accurate, or actually stripping credential headers in the proxy function.

Other comments:

  • The "proxy at the CDN level" suggestion in the warning may not be actionable for non-Cloudflare deployments — suggested mentioning API routes as a more universal alternative.
  • Test for multiple external rewrites asserts "3 external rewrite" which matches both singular and plural — suggested tightening to "3 external rewrites".
  • Acknowledged the Content-Disposition sanitization test as a good addition.

The Content-Disposition changes look solid — consistent allowlist approach across all three codepaths (image-optimization, app router prod server, pages router prod server), and the worker entry is covered via handleImageOptimization.

github run

…ding

proxyExternalRequest copies all request headers, not just credential
headers. Updated warning text to accurately state this and suggest
server-side fetch as an alternative. Tightened test assertion for
exact plural form.
@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good PR — the Content-Disposition sanitization is correct and consistent across all codepaths, and the external rewrite warning is a useful security signal. A few observations, one of which is substantive.

Content-Disposition

The allowlist approach (=== "attachment" ? "attachment" : "inline") is the right pattern. All three codepaths (image-optimization, app router prod server, pages router prod server) are updated consistently. The worker entry delegates to handleImageOptimization so it's covered transitively. The new test exercises the invalid-value boundary. This part is solid.

External rewrite warning

The implementation is clean and the test coverage is good. One accuracy issue and one minor nit.

console.warn(
`[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to third-party origins:\n` +
`${listing}\n` +
`All request headers (including cookies, authorization tokens, and other credentials) are forwarded to the external origin. ` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning text is accurate about what happens (all request headers are forwarded), which is good — the earlier revision that listed specific headers was misleading.

However, the warning describes the symptom but doesn't address the cause. proxyExternalRequest (in config-matchers.ts:940-953) copies all request headers and only strips connection and x-middleware-*. The HOP_BY_HOP_HEADERS set (which includes proxy-authorization) is only applied to the response from the upstream, not to the outbound request.

This means proxy-authorization from the client is forwarded to the external origin on the request side — it's only stripped from the upstream's response. This is the actual security gap that issue #414 is about.

The warning is good as a short-term measure, but the real fix should be stripping credential headers from the outbound request in proxyExternalRequest. That's a separate change and probably out of scope for this PR, but worth noting — the warning alone doesn't mitigate the credential leakage, it just makes it visible.

Consider filing a follow-up issue (or noting in the PR description) that proxyExternalRequest should strip authorization, cookie, proxy-authorization, etc. from outbound requests by default (with an opt-in to forward them). That would be the actual security fix.

`[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to third-party origins:\n` +
`${listing}\n` +
`All request headers (including cookies, authorization tokens, and other credentials) are forwarded to the external origin. ` +
`If this is unintentional, consider using a server-side fetch in an API route or proxying at the CDN level instead.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion to use "a server-side fetch in an API route" is good and works for all deployment targets. The "proxying at the CDN level" suggestion is Cloudflare-specific. Since vinext's primary target is Cloudflare Workers this is reasonable, but it could be slightly clearer:

Suggested change
`If this is unintentional, consider using a server-side fetch in an API route or proxying at the CDN level instead.`,
`If this is unintentional, consider using a server-side fetch in an API route (which gives you control over which headers are forwarded) or proxying at the CDN/edge level instead.`,

(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("1 external rewrite");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "1 external rewrite" also matches "1 external rewrites" (hypothetical pluralization bug). Since the code explicitly handles singular/plural, the test should verify it:

Suggested change
expect(externalWarning![0]).toContain("1 external rewrite");
expect(externalWarning![0]).toContain("1 external rewrite ");

The trailing space after rewrite ensures it doesn't match "1 external rewrites". Alternatively, use a regex match for the exact phrase.

(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("3 external rewrites");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit as above — this correctly tests the plural form, but the assertion "3 external rewrites" would also match a (very unlikely) string like "3 external rewrites-foo". This is fine in practice, just noting for consistency with the singular test.

}),
};
const response = await handleImageOptimization(request, handlers, undefined, {
contentDispositionType: "bogus" as "inline",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test — this directly exercises the sanitization boundary. The as "inline" cast is the right way to test the runtime guard while keeping TypeScript happy.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 10, 2026

Review submitted. Here's a summary of the feedback:

Content-Disposition sanitization — Looks good. The allowlist approach (=== "attachment" ? "attachment" : "inline") is correct and applied consistently across all three codepaths. The test for invalid values is well-written.

External rewrite warning — Clean implementation with good test coverage. Key feedback:

  1. Substantive: The warning describes the problem (all headers forwarded) but doesn't fix it. proxyExternalRequest only strips connection and x-middleware-* from outbound requests. The HOP_BY_HOP_HEADERS set (which includes proxy-authorization) is only applied to the response side. The real security fix would be stripping credential headers from the outbound request in proxyExternalRequest — suggested filing a follow-up for that.

  2. Suggestion: Slightly improve the alternative suggestion text to clarify why an API route is better (you control which headers are forwarded).

  3. Nit: The singular test "1 external rewrite" also matches "1 external rewrites" — suggested tightening with a trailing space.

github run

Clarify why API routes are preferable (control over forwarded headers),
tighten test assertion to prevent false singular/plural match.

Refs: cloudflare#430
@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Overall this is a solid, well-scoped PR. The Content-Disposition sanitization is correct and consistently applied across all codepaths. The external rewrite warning is well-implemented with good test coverage. A few comments below — one substantive, the rest minor.

The prior bonk reviews already flagged the key architectural point: the warning is a visibility measure, but proxyExternalRequest still forwards all request headers (including authorization, cookie, proxy-authorization) to external origins. The HOP_BY_HOP_HEADERS set is only applied to the response side. A follow-up to actually strip credential headers on the outbound request would be the real security fix. I'd suggest noting this explicitly in the PR description or filing a tracking issue, so it doesn't get lost.

(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("1 external rewrite that");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The assertion "1 external rewrite that" is tight enough to distinguish singular from plural (since the plural would produce "2 external rewrites that"). Good — this is more precise than what the earlier reviews suggested, since the trailing " that" prevents false matches against "1 external rewrites".

`[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to third-party origins:\n` +
`${listing}\n` +
`All request headers (including cookies, authorization tokens, and other credentials) are forwarded to the external origin. ` +
`If this is unintentional, consider using a server-side fetch in an API route (which gives you control over which headers are forwarded) or proxying at the CDN/edge level instead.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning text is accurate and the mitigation suggestions are reasonable. One small improvement: the "API route" suggestion could be slightly more explicit about why it's better, since less experienced developers may not immediately see the connection.

Suggested change
`If this is unintentional, consider using a server-side fetch in an API route (which gives you control over which headers are forwarded) or proxying at the CDN/edge level instead.`,
`If this is unintentional, consider using a server-side fetch in an API route or route handler (where you control exactly which headers are forwarded) or proxying at the CDN/edge level instead.`,


// Warn about external rewrites that act as reverse proxies
{
const allRewrites = [...rewrites.beforeFiles, ...rewrites.afterFiles, ...rewrites.fallback];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substantive: This warning fires at config-resolution time (during resolveNextConfig), which means it runs once on dev server startup and once during build. For production servers that load a pre-resolved config, this code path isn't hit — so the warning won't appear in production logs.

Is that intentional? If someone deploys with external rewrites, they'll see the warning during vite dev and vite build but not when the production server actually starts handling traffic. That seems like the right time to surface it. Consider whether the prod server startup paths (prod-server.ts or worker-entry.ts) should also warn.

This isn't a blocker — the build-time warning is already useful — but worth noting for a follow-up.

"X-Content-Type-Options": "nosniff",
"Content-Disposition": imageConfig?.contentDispositionType ?? "inline",
"Content-Disposition":
imageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sanitization pattern === "attachment" ? "attachment" : "inline" is correct and consistent across all three locations (image-optimization.ts:176, prod-server.ts:603, prod-server.ts:777). Good that this is an allowlist rather than a blocklist — any unexpected value falls back to the safe default.

}),
};
const response = await handleImageOptimization(request, handlers, undefined, {
contentDispositionType: "bogus" as "inline",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test. The as "inline" cast is the right approach to test the runtime guard while satisfying the type system. This ensures the sanitization works even if the config is loaded from an untrusted source that bypasses TypeScript checks.

(call) => typeof call[0] === "string" && call[0].includes("external rewrite"),
);
expect(externalWarning).toBeDefined();
expect(externalWarning![0]).toContain("3 external rewrites");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: consider also asserting that all three sources appear in the listing, to verify that beforeFiles, afterFiles, and fallback are all being checked:

Suggested change
expect(externalWarning![0]).toContain("3 external rewrites");
expect(externalWarning![0]).toContain("3 external rewrites");
// Verify all three rewrite phases are included
expect(externalWarning![0]).toContain("/proxy1");
expect(externalWarning![0]).toContain("/proxy2");
expect(externalWarning![0]).toContain("/proxy3");

The destination URLs are already asserted, but asserting the source paths too ensures the source → destination listing format is correct for all phases.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 11, 2026

Review posted successfully. Here's a summary of my feedback:

Overall: Solid, well-scoped PR. Both changes are correct and consistently applied.

Key points raised:

  1. Substantive — Warning timing: The external rewrite warning fires during resolveNextConfig (dev startup / build time), but not when the production server actually starts handling traffic. Worth considering whether prod server startup should also warn, since that's when credentials are actually being forwarded.

  2. Substantive — Follow-up needed: Echoed the prior reviews' point that the warning is a visibility measure, but proxyExternalRequest still forwards all request headers to external origins. The HOP_BY_HOP_HEADERS set is only applied to the response side. Suggested filing a tracking issue for actually stripping credential headers on the outbound request.

  3. Suggestion: Minor wording improvement on the mitigation advice to clarify why an API route is better (you control which headers are forwarded).

  4. Test improvement: Suggested also asserting source paths in the multi-rewrite test to verify the source → destination listing format across all rewrite phases.

  5. Positive notes: The Content-Disposition allowlist pattern is correct and consistent across all three codepaths. The "bogus" as "inline" test is a good approach to exercise runtime guards. The singular test assertion "1 external rewrite that" is tight enough to prevent false matches.

github run

Divkix added 3 commits March 10, 2026 17:34
…rtions

Mention route handlers alongside API routes in the warning suggestion,
and assert that source paths (/proxy1, /proxy2, /proxy3) appear in the
multi-rewrite warning message.
External rewrites via proxyExternalRequest now strip authorization,
cookie, proxy-authorization, and x-api-key headers before forwarding
to third-party origins. This prevents leaking user credentials to
external services.

Updated the warning message to reflect that credentials are stripped
(not forwarded) and updated tests to assert the new behavior.

Closes cloudflare#430
The integration test in app-router.test.ts was missed when c4a128d added
credential header stripping to proxyExternalRequest. Assertions now
correctly expect credentials to be undefined (stripped), matching the
unit test in shims.test.ts and the actual implementation.
@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR is well-scoped and addresses two concrete security concerns: credential header stripping on external rewrite proxying, and Content-Disposition sanitization for image responses. Both changes are correct, consistent across codepaths, and well-tested.

Credential header stripping

The key improvement here is that proxyExternalRequest now actually strips authorization, cookie, proxy-authorization, and x-api-key from outbound requests — and the warning message in resolveNextConfig accurately describes this behavior. Earlier revisions of the PR (flagged by prior reviews) had a mismatch where the warning listed headers but the code didn't strip them. That's been resolved.

The CREDENTIAL_HEADERS list in config-matchers.ts:957 and the warning text in next-config.ts:452 are consistent.

Content-Disposition sanitization

The allowlist pattern (=== "attachment" ? "attachment" : "inline") is the right approach — any unexpected or attacker-controlled value falls back to the safe default. Applied consistently across all three codepaths (image-optimization, app router prod server, pages router prod server), and the worker entry delegates to handleImageOptimization so it's covered transitively.

Tests

Good coverage:

  • The shims.test.ts changes correctly flip from "forwards" to "strips" for credential headers
  • The app-router.test.ts integration test verifies the full request flow
  • The new next-config.test.ts tests cover singular/plural, multi-phase rewrites, internal-only, and no-rewrites cases
  • The "bogus" as "inline" Content-Disposition test exercises the runtime guard

Two minor comments below, neither blocking.


// Strip credential headers to prevent leaking secrets to third-party origins.
// Users who need to forward credentials should use API routes or route handlers.
const CREDENTIAL_HEADERS = ["authorization", "cookie", "proxy-authorization", "x-api-key"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: CREDENTIAL_HEADERS is re-allocated on every call to proxyExternalRequest. Since this is a constant, consider hoisting it to module scope (similar to HOP_BY_HOP_HEADERS at line 161) so it's allocated once:

Suggested change
const CREDENTIAL_HEADERS = ["authorization", "cookie", "proxy-authorization", "x-api-key"];
const CREDENTIAL_HEADERS = ["authorization", "cookie", "proxy-authorization", "x-api-key"];

(moved to module level, and the inline const on line 957 removed)

Not a correctness issue — just a minor consistency/efficiency thing.

`${listing}\n` +
`Credential headers (cookie, authorization, proxy-authorization, x-api-key) are stripped from outbound requests. ` +
`All other request headers are still forwarded. ` +
`If you need to forward credentials to the external origin, use an API route or route handler where you control exactly which headers are sent.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guidance here is good and actionable. One thought: the message says "use an API route or route handler where you control exactly which headers are sent" — it might be worth adding that credentials can be injected server-side in the API route (e.g., from environment variables or a secrets store) rather than forwarded from the client request. That makes the security benefit more concrete for developers who aren't sure why an API route is better.

Not blocking — the current wording is clear enough.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 11, 2026

Review submitted with approval. Here's a summary of what I found:

Approved -- this is a clean, well-scoped security PR.

Key findings:

  1. Credential header stripping (config-matchers.ts:955-960) -- proxyExternalRequest now strips authorization, cookie, proxy-authorization, and x-api-key from outbound requests to external origins. The warning message in next-config.ts:452 accurately reflects this behavior. Prior review rounds had flagged a mismatch (warning listed headers but code didn't strip them) -- that's been resolved in this version.

  2. Content-Disposition sanitization -- The allowlist pattern (=== "attachment" ? "attachment" : "inline") is correctly applied in all three codepaths: image-optimization.ts:176, prod-server.ts:603, and prod-server.ts:777. Any unexpected value falls back to the safe "inline" default.

  3. Tests are solid -- Good coverage of both features including edge cases (invalid Content-Disposition values, singular/plural warning text, multi-phase rewrites, internal-only rewrites).

Two minor (non-blocking) comments left:

  • CREDENTIAL_HEADERS could be hoisted to module scope like HOP_BY_HOP_HEADERS instead of being re-allocated on every proxy call
  • The API route guidance in the warning could mention that credentials should be injected server-side (from env vars/secrets) rather than forwarded from the client

github run

Copy link
Collaborator

@james-elicx james-elicx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, the header stripping is a deviation from Next.js behaviour and could potentially break how users expect rewrites to work (speaking from experience, it absolutely will break use cases). Can we revert that please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants