From 2640c277fc12072cef6e2fa9de97b09965cec11a Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 11:37:51 -0700 Subject: [PATCH 1/8] fix: warn on external rewrites and sanitize Content-Disposition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #414 --- packages/vinext/src/config/next-config.ts | 16 ++++ .../vinext/src/server/image-optimization.ts | 5 +- packages/vinext/src/server/prod-server.ts | 6 +- tests/next-config.test.ts | 77 +++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index d4653127..f89c8ec1 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -10,6 +10,7 @@ import fs from "node:fs"; import { randomUUID } from "node:crypto"; import { PHASE_DEVELOPMENT_SERVER } from "../shims/constants.js"; import { normalizePageExtensions } from "../routing/file-matcher.js"; +import { isExternalUrl } from "./config-matchers.js"; /** * Parse a body size limit value (string or number) into bytes. @@ -438,6 +439,21 @@ export async function resolveNextConfig( } } + // 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.`, + ); + } + } + // Resolve headers let headers: NextHeader[] = []; if (config.headers) { diff --git a/packages/vinext/src/server/image-optimization.ts b/packages/vinext/src/server/image-optimization.ts index 07cdb661..9747f3ce 100644 --- a/packages/vinext/src/server/image-optimization.ts +++ b/packages/vinext/src/server/image-optimization.ts @@ -171,7 +171,10 @@ function setImageSecurityHeaders(headers: Headers, config?: ImageConfig): void { config?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY, ); headers.set("X-Content-Type-Options", "nosniff"); - headers.set("Content-Disposition", config?.contentDispositionType ?? "inline"); + headers.set( + "Content-Disposition", + config?.contentDispositionType === "attachment" ? "attachment" : "inline", + ); } /** diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index a11ce3b8..c7ff1a98 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -599,7 +599,8 @@ async function startAppRouterServer(options: AppRouterServerOptions) { "Content-Security-Policy": imageConfig?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY, "X-Content-Type-Options": "nosniff", - "Content-Disposition": imageConfig?.contentDispositionType ?? "inline", + "Content-Disposition": + imageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline", }; if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) { return; @@ -772,7 +773,8 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { "Content-Security-Policy": pagesImageConfig?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY, "X-Content-Type-Options": "nosniff", - "Content-Disposition": pagesImageConfig?.contentDispositionType ?? "inline", + "Content-Disposition": + pagesImageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline", }; if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) { return; diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index 43a8851c..4c56cd64 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -596,3 +596,80 @@ describe("generateBuildId", () => { expect(b.buildId).toBe("stable-id"); }); }); + +describe("resolveNextConfig external rewrite warning", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("emits a warning when rewrites contain external destinations", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await resolveNextConfig({ + rewrites: async () => [ + { source: "/api/:path*", destination: "https://api.example.com/:path*" }, + { source: "/internal", destination: "/other" }, + ], + }); + + // 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"); + }); + + it("does not warn when all rewrites are internal", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await resolveNextConfig({ + rewrites: async () => [ + { source: "/old", destination: "/new" }, + { source: "/a", destination: "/b" }, + ], + }); + + const externalWarning = warn.mock.calls.find( + (call) => typeof call[0] === "string" && call[0].includes("external rewrite"), + ); + expect(externalWarning).toBeUndefined(); + }); + + it("warns about multiple external rewrites across beforeFiles, afterFiles, and fallback", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await resolveNextConfig({ + rewrites: async () => ({ + beforeFiles: [{ source: "/proxy1", destination: "https://one.example.com/api" }], + afterFiles: [{ source: "/proxy2", destination: "https://two.example.com/api" }], + fallback: [{ source: "/proxy3", destination: "https://three.example.com/api" }], + }), + }); + + const externalWarning = warn.mock.calls.find( + (call) => typeof call[0] === "string" && call[0].includes("external rewrite"), + ); + expect(externalWarning).toBeDefined(); + expect(externalWarning![0]).toContain("3 external rewrite"); + expect(externalWarning![0]).toContain("https://one.example.com/api"); + expect(externalWarning![0]).toContain("https://two.example.com/api"); + expect(externalWarning![0]).toContain("https://three.example.com/api"); + }); + + it("does not warn when no rewrites are configured", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await resolveNextConfig({ env: {} }); + + const externalWarning = warn.mock.calls.find( + (call) => typeof call[0] === "string" && call[0].includes("external rewrite"), + ); + expect(externalWarning).toBeUndefined(); + }); +}); From c9fdd964a96475ac240b1cf1c450436b705e8ed3 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 11:49:03 -0700 Subject: [PATCH 2/8] fix: improve external rewrite warning and add Content-Disposition sanitization 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". --- packages/vinext/src/config/next-config.ts | 3 ++- tests/next-config.test.ts | 2 ++ tests/shims.test.ts | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index f89c8ec1..c3a192e4 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -445,8 +445,9 @@ export async function resolveNextConfig( const externalRewrites = allRewrites.filter((r) => isExternalUrl(r.destination)); if (externalRewrites.length > 0) { const listing = externalRewrites.map((r) => ` ${r.source} → ${r.destination}`).join("\n"); + const noun = externalRewrites.length === 1 ? "external rewrite" : "external rewrites"; console.warn( - `[vinext] Found ${externalRewrites.length} external rewrite(s) that proxy requests to third-party origins:\n` + + `[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.`, diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index 4c56cd64..c7f8b66a 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -620,6 +620,8 @@ describe("resolveNextConfig external rewrite warning", () => { expect(externalWarning![0]).toContain("1 external rewrite"); expect(externalWarning![0]).toContain("https://api.example.com/:path*"); expect(externalWarning![0]).toContain("Credential headers"); + expect(externalWarning![0]).toContain("/api/:path*"); + expect(externalWarning![0]).toContain("→"); // The internal rewrite should not appear in the warning expect(externalWarning![0]).not.toContain("/other"); diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 6150d82e..9f81a9f3 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -6988,6 +6988,24 @@ describe("handleImageOptimization", () => { expect(response.headers.get("Content-Disposition")).toBe("attachment"); }); + it("defaults Content-Disposition to inline when contentDispositionType is invalid", async () => { + const { handleImageOptimization } = + await import("../packages/vinext/src/server/image-optimization.js"); + const request = new Request("http://localhost/_vinext/image?url=%2Fimg.jpg&w=800"); + const handlers = { + fetchAsset: async () => + new Response("image-data", { + status: 200, + headers: { "Content-Type": "image/jpeg" }, + }), + }; + const response = await handleImageOptimization(request, handlers, undefined, { + contentDispositionType: "bogus" as "inline", + }); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Disposition")).toBe("inline"); + }); + it("applies custom contentSecurityPolicy", async () => { const { handleImageOptimization } = await import("../packages/vinext/src/server/image-optimization.js"); From 96b2916d6060f65f534cf02f0575046212ebfe4f Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 16:13:32 -0700 Subject: [PATCH 3/8] fix: correct external rewrite warning to reflect actual header forwarding 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. --- packages/vinext/src/config/next-config.ts | 4 ++-- tests/next-config.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index c3a192e4..a29c4b36 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -449,8 +449,8 @@ export async function resolveNextConfig( console.warn( `[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.`, + `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.`, ); } } diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index c7f8b66a..631ac190 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -619,7 +619,7 @@ describe("resolveNextConfig external rewrite warning", () => { 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"); + expect(externalWarning![0]).toContain("All request headers"); expect(externalWarning![0]).toContain("/api/:path*"); expect(externalWarning![0]).toContain("→"); @@ -658,7 +658,7 @@ describe("resolveNextConfig external rewrite warning", () => { (call) => typeof call[0] === "string" && call[0].includes("external rewrite"), ); expect(externalWarning).toBeDefined(); - expect(externalWarning![0]).toContain("3 external rewrite"); + expect(externalWarning![0]).toContain("3 external rewrites"); expect(externalWarning![0]).toContain("https://one.example.com/api"); expect(externalWarning![0]).toContain("https://two.example.com/api"); expect(externalWarning![0]).toContain("https://three.example.com/api"); From 06a869d46a53a8228099d71357e6227d62bf3d02 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 17:17:35 -0700 Subject: [PATCH 4/8] fix: address PR review feedback on external rewrite warning Clarify why API routes are preferable (control over forwarded headers), tighten test assertion to prevent false singular/plural match. Refs: #430 --- packages/vinext/src/config/next-config.ts | 2 +- tests/next-config.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index a29c4b36..78b64856 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -450,7 +450,7 @@ export async function resolveNextConfig( `[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.`, + `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.`, ); } } diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index 631ac190..b6296c24 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -617,7 +617,7 @@ describe("resolveNextConfig external rewrite warning", () => { (call) => typeof call[0] === "string" && call[0].includes("external rewrite"), ); expect(externalWarning).toBeDefined(); - expect(externalWarning![0]).toContain("1 external rewrite"); + expect(externalWarning![0]).toContain("1 external rewrite that"); expect(externalWarning![0]).toContain("https://api.example.com/:path*"); expect(externalWarning![0]).toContain("All request headers"); expect(externalWarning![0]).toContain("/api/:path*"); From affd28e79ca31bf35630b861005a8c69e7cabf51 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 17:34:44 -0700 Subject: [PATCH 5/8] fix: refine external rewrite warning wording and add source path assertions 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. --- packages/vinext/src/config/next-config.ts | 2 +- tests/next-config.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index 78b64856..b6d611b8 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -450,7 +450,7 @@ export async function resolveNextConfig( `[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.`, + `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.`, ); } } diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index b6296c24..457d5d81 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -662,6 +662,9 @@ describe("resolveNextConfig external rewrite warning", () => { expect(externalWarning![0]).toContain("https://one.example.com/api"); expect(externalWarning![0]).toContain("https://two.example.com/api"); expect(externalWarning![0]).toContain("https://three.example.com/api"); + expect(externalWarning![0]).toContain("/proxy1"); + expect(externalWarning![0]).toContain("/proxy2"); + expect(externalWarning![0]).toContain("/proxy3"); }); it("does not warn when no rewrites are configured", async () => { From c4a128d13e8f4cc11175cce7773d417c530b7001 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 17:42:04 -0700 Subject: [PATCH 6/8] fix: strip credential headers from outbound proxy requests 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 #430 --- packages/vinext/src/config/config-matchers.ts | 7 +++++++ packages/vinext/src/config/next-config.ts | 5 +++-- tests/next-config.test.ts | 3 ++- tests/shims.test.ts | 12 ++++++------ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/vinext/src/config/config-matchers.ts b/packages/vinext/src/config/config-matchers.ts index ab02d4fa..72c12900 100644 --- a/packages/vinext/src/config/config-matchers.ts +++ b/packages/vinext/src/config/config-matchers.ts @@ -952,6 +952,13 @@ export async function proxyExternalRequest( headers.delete(key); } + // 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"]; + for (const key of CREDENTIAL_HEADERS) { + headers.delete(key); + } + const method = request.method; const hasBody = method !== "GET" && method !== "HEAD"; diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index b6d611b8..5dc77b31 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -449,8 +449,9 @@ export async function resolveNextConfig( 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. ` + - `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.`, + `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.`, ); } } diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index 457d5d81..9b198d29 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -619,7 +619,8 @@ describe("resolveNextConfig external rewrite warning", () => { expect(externalWarning).toBeDefined(); expect(externalWarning![0]).toContain("1 external rewrite that"); expect(externalWarning![0]).toContain("https://api.example.com/:path*"); - expect(externalWarning![0]).toContain("All request headers"); + expect(externalWarning![0]).toContain("Credential headers"); + expect(externalWarning![0]).toContain("stripped"); expect(externalWarning![0]).toContain("/api/:path*"); expect(externalWarning![0]).toContain("→"); diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 9f81a9f3..8574e78e 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -4162,7 +4162,7 @@ describe("proxyExternalRequest", () => { } }); - it("forwards credential headers and strips x-middleware-* headers from proxied requests", async () => { + it("strips credential and x-middleware-* headers from proxied requests", async () => { const { proxyExternalRequest } = await import("../packages/vinext/src/config/config-matchers.js"); @@ -4190,11 +4190,11 @@ describe("proxyExternalRequest", () => { try { await proxyExternalRequest(request, "https://api.example.com/data"); expect(capturedHeaders).toBeDefined(); - // Credential headers must be forwarded (matching Next.js behavior) - expect(capturedHeaders!.get("cookie")).toBe("session=secret123"); - expect(capturedHeaders!.get("authorization")).toBe("Bearer tok_secret"); - expect(capturedHeaders!.get("x-api-key")).toBe("sk_live_secret"); - expect(capturedHeaders!.get("proxy-authorization")).toBe("Basic cHJveHk="); + // Credential headers must be stripped to prevent leaking secrets + expect(capturedHeaders!.get("cookie")).toBeNull(); + expect(capturedHeaders!.get("authorization")).toBeNull(); + expect(capturedHeaders!.get("x-api-key")).toBeNull(); + expect(capturedHeaders!.get("proxy-authorization")).toBeNull(); // Internal middleware headers must be stripped expect(capturedHeaders!.get("x-middleware-rewrite")).toBeNull(); expect(capturedHeaders!.get("x-middleware-next")).toBeNull(); From 8a5f972bf11c56a7924c20252d7d359e79ca2ad8 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Tue, 10 Mar 2026 17:50:13 -0700 Subject: [PATCH 7/8] fix: align app-router credential stripping test with implementation 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. --- tests/app-router.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index 532f6c52..cf3a958f 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -3073,7 +3073,7 @@ describe("App Router external rewrite proxy credential stripping", () => { await new Promise((resolve) => mockServer?.close(() => resolve())); }); - it("forwards credential headers through proxied requests to external rewrite targets", async () => { + it("strips credential and x-middleware-* headers from proxied requests to external rewrite targets", async () => { mockResponseMode = "plain"; capturedHeaders = null; @@ -3089,11 +3089,11 @@ describe("App Router external rewrite proxy credential stripping", () => { }); expect(capturedHeaders).not.toBeNull(); - // Credential headers must be forwarded (matching Next.js behavior) - expect(capturedHeaders!["cookie"]).toBe("session=secret123"); - expect(capturedHeaders!["authorization"]).toBe("Bearer tok_secret"); - expect(capturedHeaders!["x-api-key"]).toBe("sk_live_secret"); - expect(capturedHeaders!["proxy-authorization"]).toBe("Basic cHJveHk="); + // Credential headers must be stripped to prevent leaking secrets + expect(capturedHeaders!["cookie"]).toBeUndefined(); + expect(capturedHeaders!["authorization"]).toBeUndefined(); + expect(capturedHeaders!["x-api-key"]).toBeUndefined(); + expect(capturedHeaders!["proxy-authorization"]).toBeUndefined(); // Internal middleware headers must be stripped expect(capturedHeaders!["x-middleware-next"]).toBeUndefined(); // Non-sensitive headers must be preserved From afd70488cadd9581b07eb0a51f9371c7bd4f84b2 Mon Sep 17 00:00:00 2001 From: Divanshu Chauhan Date: Wed, 11 Mar 2026 08:33:34 -0700 Subject: [PATCH 8/8] fix: keep Next.js rewrite forwarding --- packages/vinext/src/config/next-config.ts | 17 ++++++++++------- tests/app-router.test.ts | 6 +++--- tests/next-config.test.ts | 9 ++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index 5dc77b31..e6e969f1 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -439,19 +439,22 @@ export async function resolveNextConfig( } } - // 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)); + const externalRewrites = allRewrites.filter((rewrite) => isExternalUrl(rewrite.destination)); + if (externalRewrites.length > 0) { - const listing = externalRewrites.map((r) => ` ${r.source} → ${r.destination}`).join("\n"); const noun = externalRewrites.length === 1 ? "external rewrite" : "external rewrites"; + const listing = externalRewrites + .map((rewrite) => ` ${rewrite.source} → ${rewrite.destination}`) + .join("\n"); + console.warn( - `[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to third-party origins:\n` + + `[vinext] Found ${externalRewrites.length} ${noun} that proxy requests to external origins:\n` + `${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.`, + `Request headers, including credential headers (cookie, authorization, proxy-authorization, x-api-key), ` + + `are forwarded to the external origin to match Next.js behavior. ` + + `If you do not want to forward credentials, use an API route or route handler where you control exactly which headers are sent.`, ); } } diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index cf3a958f..95df1746 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -3024,11 +3024,11 @@ describe("RSC plugin auto-registration", () => { }); }); -// ── External rewrite proxy credential stripping (App Router) ───────────────── +// ── External rewrite proxy credential forwarding (App Router) ──────────────── // Regression test: the proxyExternalRequest (imported from config-matchers) in the generated RSC entry -// must strip Cookie, Authorization, x-api-key, proxy-authorization, and +// must forward credential headers like Next.js while still stripping // x-middleware-* headers before forwarding to external rewrite destinations. -describe("App Router external rewrite proxy credential stripping", () => { +describe("App Router external rewrite proxy credential forwarding", () => { let mockServer: import("node:http").Server; let mockPort: number; let capturedHeaders: import("node:http").IncomingHttpHeaders | null = null; diff --git a/tests/next-config.test.ts b/tests/next-config.test.ts index 9b198d29..f6ca9728 100644 --- a/tests/next-config.test.ts +++ b/tests/next-config.test.ts @@ -612,19 +612,18 @@ describe("resolveNextConfig external rewrite warning", () => { ], }); - // 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 that"); expect(externalWarning![0]).toContain("https://api.example.com/:path*"); - expect(externalWarning![0]).toContain("Credential headers"); - expect(externalWarning![0]).toContain("stripped"); expect(externalWarning![0]).toContain("/api/:path*"); expect(externalWarning![0]).toContain("→"); - - // The internal rewrite should not appear in the warning + expect(externalWarning![0]).toContain("credential headers"); + expect(externalWarning![0]).toContain("forwarded"); + expect(externalWarning![0]).toContain("match Next.js behavior"); expect(externalWarning![0]).not.toContain("/other"); });