diff --git a/.changeset/bright-balloons-own.md b/.changeset/bright-balloons-own.md new file mode 100644 index 000000000..1bd0ff691 --- /dev/null +++ b/.changeset/bright-balloons-own.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +Fix a security vulnerability similar to the recent CVE-2025-29927 diff --git a/packages/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts index 0420bd0c1..a309d01d9 100644 --- a/packages/open-next/src/core/routing/middleware.ts +++ b/packages/open-next/src/core/routing/middleware.ts @@ -4,6 +4,7 @@ import { FunctionsConfigManifest, MiddlewareManifest, NextConfig, + PrerenderManifest, } from "config/index.js"; import type { InternalEvent, InternalResult } from "types/open-next.js"; import { emptyReadableStream } from "utils/stream.js"; @@ -57,7 +58,14 @@ export async function handleMiddleware( const headers = internalEvent.headers; // We bypass the middleware if the request is internal - if (headers["x-isr"]) return internalEvent; + // We should only do that if the request has the correct `x-prerender-revalidate` header + // The `x-prerender-revalidate` header is set at build time and should be safe to trust + if ( + headers["x-isr"] && + headers["x-prerender-revalidate"] === + PrerenderManifest.preview.previewModeId + ) + return internalEvent; // We only need the normalizedPath to check if the middleware should run const normalizedPath = localizePath(internalEvent); diff --git a/packages/open-next/src/types/next-types.ts b/packages/open-next/src/types/next-types.ts index cac694f9a..d671c0087 100644 --- a/packages/open-next/src/types/next-types.ts +++ b/packages/open-next/src/types/next-types.ts @@ -174,6 +174,9 @@ export interface PrerenderManifest { dataRouteRegex: string; }; }; + preview: { + previewModeId: string; + }; } export type Options = { diff --git a/packages/tests-unit/tests/core/routing/middleware.test.ts b/packages/tests-unit/tests/core/routing/middleware.test.ts index 066d1a711..149b887ff 100644 --- a/packages/tests-unit/tests/core/routing/middleware.test.ts +++ b/packages/tests-unit/tests/core/routing/middleware.test.ts @@ -33,6 +33,11 @@ vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ version: 2, }, FunctionsConfigManifest: undefined, + PrerenderManifest: { + preview: { + previewModeId: "preview", + }, + }, })); vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({ @@ -76,6 +81,7 @@ describe("handleMiddleware", () => { const event = createEvent({ headers: { "x-isr": "1", + "x-prerender-revalidate": "preview", }, }); const result = await handleMiddleware(event, middlewareLoader); @@ -84,6 +90,45 @@ describe("handleMiddleware", () => { expect(result).toEqual(event); }); + it("should not bypass middleware for request with an incorrect x-prerender-revalidate", async () => { + const event = createEvent({ + headers: { + "x-isr": "1", + "x-prerender-revalidate": "incorrect", + }, + }); + middleware.mockResolvedValue({ + status: 302, + headers: new Headers({ + location: "/redirect", + }), + }); + const result = await handleMiddleware(event, middlewareLoader); + + expect(middlewareLoader).toHaveBeenCalled(); + expect(result.statusCode).toEqual(302); + expect(result.headers.location).toEqual("/redirect"); + }); + + it("should not bypass middleware if there is no x-prerender-revalidate", async () => { + const event = createEvent({ + headers: { + "x-isr": "1", + }, + }); + middleware.mockResolvedValue({ + status: 302, + headers: new Headers({ + location: "/redirect", + }), + }); + const result = await handleMiddleware(event, middlewareLoader); + + expect(middlewareLoader).toHaveBeenCalled(); + expect(result.statusCode).toEqual(302); + expect(result.headers.location).toEqual("/redirect"); + }); + it("should invoke middleware with redirect", async () => { const event = createEvent({}); middleware.mockResolvedValue({