VYPR
High severity8.2NVD Advisory· Published Apr 25, 2025· Updated Apr 15, 2026

CVE-2025-43865

CVE-2025-43865

Description

React Router is a router for React. In versions on the 7.0 branch prior to version 7.5.2, it's possible to modify pre-rendered data by adding a header to the request. This allows to completely spoof its contents and modify all the values ​​of the data object passed to the HTML. 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.0.0-pre.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.