VYPR
High severity7.5NVD Advisory· Published Apr 25, 2025· Updated Apr 15, 2026

CVE-2025-43864

CVE-2025-43864

Description

React Router is a router for React. Starting in version 7.2.0 and prior to version 7.5.2, it is possible to force an application to switch to SPA mode by adding a header to the request. If the application uses SSR and is forced to switch to SPA, this causes an error that completely corrupts the page. If a cache system is in place, this allows the response containing the error to be cached, resulting in a cache poisoning that strongly impacts the availability of the application. This issue has been patched in version 7.5.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
react-routernpm
>= 7.2.0, < 7.5.27.5.2

Patches

2
c84302972a15

Adjust approach for prerendering/SPA mode via headers (#13453)

https://github.com/remix-run/react-routerMatt BrophyApr 24, 2025via ghsa
7 files changed · +95 31
  • .changeset/stale-bats-swim.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +"@react-router/dev": patch
    +"react-router": patch
    +---
    +
    +Adjust approach for Prerendering/SPA Mode via headers
    
  • integration/vite-prerender-test.ts+17 5 modified
    @@ -602,7 +602,7 @@ test.describe("Prerendering", () => {
               "app/routes/about.tsx": js`
                 import { useLoaderData } from 'react-router';
                 export function loader({ request }) {
    -              return "ABOUT-" + request.headers.has('X-React-Router-Prerender');
    +              return "ABOUT-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
                 }
     
                 export default function Comp() {
    @@ -613,7 +613,7 @@ test.describe("Prerendering", () => {
               "app/routes/not-prerendered.tsx": js`
                 import { useLoaderData } from 'react-router';
                 export function loader({ request }) {
    -              return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender');
    +              return "NOT-PRERENDERED-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
                 }
     
                 export default function Comp() {
    @@ -659,7 +659,7 @@ test.describe("Prerendering", () => {
                 import { useLoaderData } from 'react-router';
                 export function loader({ request }) {
                   return {
    -                prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
    +                prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
                     // 24999 characters
                     data: new Array(5000).fill('test').join('-'),
                   };
    @@ -712,7 +712,7 @@ test.describe("Prerendering", () => {
                 import { useLoaderData } from 'react-router';
                 export function loader({ request }) {
                   return {
    -                prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
    +                prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
                     data: "한글 데이터 - UTF-8 문자",
                   };
                 }
    @@ -732,7 +732,7 @@ test.describe("Prerendering", () => {
                 import { useLoaderData } from 'react-router';
                 export function loader({ request }) {
                   return {
    -                prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
    +                prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
                     data: "非プリレンダリングデータ - UTF-8文字",
                   };
                 }
    @@ -837,6 +837,18 @@ test.describe("Prerendering", () => {
           await page.waitForSelector("[data-mounted]");
           expect(await app.getHtml()).toMatch("Index: INDEX");
         });
    +
    +    test("Ignores build-time headers at runtime", async () => {
    +      fixture = await createFixture({ files });
    +      let res = await fixture.requestSingleFetchData("/_root.data", {
    +        headers: {
    +          "X-React-Router-Prerender-Data": encodeURI(
    +            '[{"_1":2},"routes/_index",{"_3":4},"data","Hello World!"]'
    +          ),
    +        },
    +      });
    +      expect((res.data as any)["routes/_index"].data).toBe("Index Loader Data");
    +    });
       });
     
       test.describe("ssr: false", () => {
    
  • integration/vite-spa-mode-test.ts+31 0 modified
    @@ -234,6 +234,37 @@ test.describe("SPA Mode", () => {
               expect(await res.text()).toMatch(/^<!DOCTYPE html><html lang="en">/);
             });
     
    +        test("Ignores build-time headers at runtime", async () => {
    +          let fixture = await createFixture({
    +            files: {
    +              "react-router.config.ts": reactRouterConfig({
    +                splitRouteModules,
    +              }),
    +              "app/root.tsx": js`
    +                import { Outlet, Scripts } from "react-router";
    +
    +                export default function Root() {
    +                  return (
    +                    <html lang="en">
    +                      <head></head>
    +                      <body>
    +                        <h1 data-root>Root</h1>
    +                        <Scripts />
    +                      </body>
    +                    </html>
    +                  );
    +                }
    +              `,
    +            },
    +          });
    +          let res = await fixture.requestDocument("/", {
    +            headers: { "X-React-Router-SPA-Mode": "yes" },
    +          });
    +          let html = await res.text();
    +          expect(html).toMatch('"isSpaMode":false');
    +          expect(html).toMatch('<h1 data-root="true">Root</h1>');
    +        });
    +
             test("works when combined with a basename", async ({ page }) => {
               fixture = await createFixture({
                 spaMode: true,
    
  • packages/react-router-dev/vite/plugin.ts+11 16 modified
    @@ -1724,6 +1724,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
                 );
               }
     
    +          // Set an environment variable we can look for in the handler to
    +          // enable some build-time-only logic
    +          process.env.IS_RR_BUILD_REQUEST = "yes";
    +
               if (isPrerenderingEnabled(ctx.reactRouterConfig)) {
                 // If we have prerender routes, that takes precedence over SPA mode
                 // which is ssr:false and only the root route being rendered
    @@ -2623,11 +2627,6 @@ async function handlePrerender(
       }
     
       let buildRoutes = createPrerenderRoutes(build.routes);
    -  let headers = {
    -    // Header that can be used in the loader to know if you're running at
    -    // build time or runtime
    -    "X-React-Router-Prerender": "yes",
    -  };
       for (let path of build.prerender) {
         // Ensure we have a leading slash for matching
         let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/"));
    @@ -2655,17 +2654,15 @@ async function handlePrerender(
               [leafRoute.id],
               clientBuildDirectory,
               reactRouterConfig,
    -          viteConfig,
    -          { headers }
    +          viteConfig
             );
             // Prerender a raw file for external consumption
             await prerenderResourceRoute(
               handler,
               path,
               clientBuildDirectory,
               reactRouterConfig,
    -          viteConfig,
    -          { headers }
    +          viteConfig
             );
           } else {
             viteConfig.logger.warn(
    @@ -2684,8 +2681,7 @@ async function handlePrerender(
               null,
               clientBuildDirectory,
               reactRouterConfig,
    -          viteConfig,
    -          { headers }
    +          viteConfig
             );
           }
     
    @@ -2698,11 +2694,10 @@ async function handlePrerender(
             data
               ? {
                   headers: {
    -                ...headers,
                     "X-React-Router-Prerender-Data": encodeURI(data),
                   },
                 }
    -          : { headers }
    +          : undefined
           );
         }
       }
    @@ -2746,7 +2741,7 @@ async function prerenderData(
       clientBuildDirectory: string,
       reactRouterConfig: ResolvedReactRouterConfig,
       viteConfig: Vite.ResolvedConfig,
    -  requestInit: RequestInit
    +  requestInit?: RequestInit
     ) {
       let normalizedPath = `${reactRouterConfig.basename}${
         prerenderPath === "/"
    @@ -2789,7 +2784,7 @@ async function prerenderRoute(
       clientBuildDirectory: string,
       reactRouterConfig: ResolvedReactRouterConfig,
       viteConfig: Vite.ResolvedConfig,
    -  requestInit: RequestInit
    +  requestInit?: RequestInit
     ) {
       let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(
         /\/\/+/g,
    @@ -2845,7 +2840,7 @@ async function prerenderResourceRoute(
       clientBuildDirectory: string,
       reactRouterConfig: ResolvedReactRouterConfig,
       viteConfig: Vite.ResolvedConfig,
    -  requestInit: RequestInit
    +  requestInit?: RequestInit
     ) {
       let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`
         .replace(/\/\/+/g, "/")
    
  • packages/react-router/lib/server-runtime/dev.ts+12 0 modified
    @@ -14,3 +14,15 @@ export function getDevServerHooks(): DevServerHooks | undefined {
       // @ts-expect-error
       return globalThis[globalDevServerHooksKey];
     }
    +
    +// Guarded access to build-time-only headers
    +export function getBuildTimeHeader(request: Request, headerName: string) {
    +  if (typeof process !== "undefined") {
    +    try {
    +      if (process.env?.IS_RR_BUILD_REQUEST === "yes") {
    +        return request.headers.get(headerName);
    +      }
    +    } catch (e) {}
    +  }
    +  return null;
    +}
    
  • packages/react-router/lib/server-runtime/routes.ts+6 4 modified
    @@ -19,6 +19,7 @@ import {
     } from "../dom/ssr/single-fetch";
     import invariant from "./invariant";
     import type { ServerRouteModule } from "../dom/ssr/routeModules";
    +import { getBuildTimeHeader } from "./dev";
     
     export type ServerRouteManifest = RouteManifest<Omit<ServerRoute, "children">>;
     
    @@ -86,10 +87,11 @@ export function createStaticHandlerDataRoutes(
             ? async (args: RRLoaderFunctionArgs) => {
                 // If we're prerendering, use the data passed in from prerendering
                 // the .data route so we don't call loaders twice
    -            if (args.request.headers.has("X-React-Router-Prerender-Data")) {
    -              const preRenderedData = args.request.headers.get(
    -                "X-React-Router-Prerender-Data"
    -              );
    +            let preRenderedData = getBuildTimeHeader(
    +              args.request,
    +              "X-React-Router-Prerender-Data"
    +            );
    +            if (preRenderedData != null) {
                   let encoded = preRenderedData
                     ? decodeURI(preRenderedData)
                     : preRenderedData;
    
  • packages/react-router/lib/server-runtime/server.ts+12 6 modified
    @@ -23,7 +23,7 @@ import { matchServerRoutes } from "./routeMatching";
     import type { ServerRoute } from "./routes";
     import { createStaticHandlerDataRoutes, createRoutes } from "./routes";
     import { createServerHandoffString } from "./serverHandoff";
    -import { getDevServerHooks } from "./dev";
    +import { getBuildTimeHeader, getDevServerHooks } from "./dev";
     import {
       encodeViaTurboStream,
       getSingleFetchRedirect,
    @@ -164,12 +164,17 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
           normalizedPath = normalizedPath.slice(0, -1);
         }
     
    +    let isSpaMode =
    +      getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes";
    +
         // When runtime SSR is disabled, make our dev server behave like the deployed
         // pre-rendered site would
         if (!_build.ssr) {
    +      // When SSR is disabled this, file can only ever run during dev because we
    +      // delete the server build at the end of the build
           if (_build.prerender.length === 0) {
    -        // Add the header if we're in SPA mode
    -        request.headers.set("X-React-Router-SPA-Mode", "yes");
    +        // ssr:false and no prerender config indicates "SPA Mode"
    +        isSpaMode = true;
           } else if (
             !_build.prerender.includes(normalizedPath) &&
             !_build.prerender.includes(normalizedPath + "/")
    @@ -194,7 +199,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
               });
             } else {
               // Serve a SPA fallback for non-pre-rendered document requests
    -          request.headers.set("X-React-Router-SPA-Mode", "yes");
    +          isSpaMode = true;
             }
           }
         }
    @@ -275,7 +280,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
             }
           }
         } else if (
    -      !request.headers.has("X-React-Router-SPA-Mode") &&
    +      !isSpaMode &&
           matches &&
           matches[matches.length - 1].route.module.default == null &&
           matches[matches.length - 1].route.module.ErrorBoundary == null
    @@ -309,6 +314,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
             request,
             loadContext,
             handleError,
    +        isSpaMode,
             criticalCss
           );
         }
    @@ -426,9 +432,9 @@ async function handleDocumentRequest(
       request: Request,
       loadContext: AppLoadContext | unstable_RouterContextProvider,
       handleError: (err: unknown) => void,
    +  isSpaMode: boolean,
       criticalCss?: CriticalCss
     ) {
    -  let isSpaMode = request.headers.has("X-React-Router-SPA-Mode");
       try {
         let response = await staticHandler.query(request, {
           requestContext: loadContext,
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.