# Root Cause Analysis: CVE-2025-29927 (Next.js Middleware Bypass)

## Summary

CVE-2025-29927 is an authorization-bypass vulnerability in self-hosted Next.js applications. Next.js uses the internal `x-middleware-subrequest` header to prevent recursive middleware execution when a middleware function internally re-issues a request. In vulnerable versions, this header is not stripped from externally originating HTTP requests, so an attacker can add `x-middleware-subrequest` with a value that matches the middleware name (e.g., `middleware:middleware:middleware:middleware:middleware`) and cause the middleware to be skipped entirely. When applications rely on middleware for authentication or authorization, this allows unauthenticated access to protected routes.

## Impact

- **Package/component affected:** `next` (npm package)
- **Affected versions:** >= 11.1.4 < 12.3.5, >= 13.0.0 < 13.5.9, >= 14.0.0 < 14.2.25, >= 15.0.0 < 15.2.3
- **Patched versions:** 12.3.5, 13.5.9, 14.2.25, 15.2.3
- **Risk level and consequences:** Critical. An unauthenticated remote attacker can bypass middleware-based access controls, including authentication and authorization checks, on self-hosted Next.js deployments. This can lead to unauthorized access to protected resources, data disclosure, and privilege escalation. Vercel-hosted deployments are not affected because the edge filters the internal header.

## Impact Parity

- **Disclosed/claimed maximum impact:** The advisory and CVE describe an authorization bypass. The bundled `ticket.json` additionally claims `library_api` surface and `code_execution` impact, which does not match the actual vulnerability.
- **Reproduced impact from this run:** We confirmed the middleware bypass on Next.js 14.2.24: a request to `/protected` without authentication is blocked with HTTP 401, while the same request with `x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware` returns HTTP 200 and the protected content. The fixed version 14.2.25 continues to block both requests with HTTP 401.
- **Parity:** `partial` — the actual authorization bypass is fully reproduced, but the submitted `library_api`/`convert_document`/`code_execution` claim does not match the observed `api_remote`/`authz_bypass` surface and impact.
- **Not demonstrated:** Code execution, memory corruption, or any other impact class beyond middleware authorization bypass and protected content disclosure.

## Root Cause

The Next.js middleware runner (`packages/next/src/server/web/sandbox/sandbox.ts`) checks the incoming request headers for `x-middleware-subrequest`. It splits the header value on `:` and compares the entries against `params.name` (the registered middleware name). If the list already contains the middleware name, the runner assumes the request is an internal subrequest generated by the middleware itself and short-circuits the execution by returning `NextResponse.next()`.

In vulnerable versions, `filterInternalHeaders` (`packages/next/src/server/lib/server-ipc/utils.ts`) strips many internal headers but does **not** strip `x-middleware-subrequest` from externally originating requests. Consequently, an attacker can inject this header and trick the server into skipping the middleware.

The fix (commits `52a078da3884efe6501613c7834a3d02a91676d2` and `5fd3ae8f8542677c6294f32d18022731eab6fe48`) introduces a per-session random `middlewareSubrequestId` stored in `globalThis[Symbol.for('@next/middleware-subrequest-id')]`:

1. The router server generates the random ID at startup.
2. When the middleware runner makes an internal `fetch`, it copies `x-middleware-subrequest` and also sets `x-middleware-subrequest-id` to the session ID.
3. `filterInternalHeaders` now deletes `x-middleware-subrequest` unless the accompanying `x-middleware-subrequest-id` matches the current session ID.

This ensures that any externally injected `x-middleware-subrequest` header is removed before the middleware runner sees it, preventing the bypass.

## Reproduction Steps

The full reproduction is implemented in `bundle/repro/reproduction_steps.sh`. At a high level, the script:

1. Creates a minimal Next.js application in the prepared project cache directory (`<project_cache_dir>/repo`) with `middleware.js` and `app/protected/page.js`.
2. Installs the vulnerable Next.js version `14.2.24`.
3. Builds the app and starts the production server with `next start` on `127.0.0.1`.
4. Performs a health check on the root route, then issues two requests to `/protected`:
   - Normal request without authentication headers → expected 401.
   - Malicious request with `x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware` → expected 200 on vulnerable, 401 on fixed.
5. Repeats the same procedure for the fixed version `14.2.25` as a negative control.
6. Writes the runtime evidence to `bundle/repro/runtime_manifest.json`.

Expected evidence of reproduction:
- Vulnerable 14.2.24: normal request returns `401`, bypass request returns `200` with body containing `secret-data`.
- Fixed 14.2.25: both normal and bypass requests return `401`.

## Evidence

Key artifacts written by the script:

- `bundle/logs/vuln-1-summary.txt` and `bundle/logs/vuln-2-summary.txt`: `[vuln attempt N] normal=401 bypass=401 bypass_poly=200`
- `bundle/logs/fixed-1-summary.txt` and `bundle/logs/fixed-2-summary.txt`: `[fixed attempt N] normal=401 bypass=401 bypass_poly=401`
- `bundle/logs/vuln-1-bypass-poly-body.html` and `bundle/logs/vuln-2-bypass-poly-body.html`: contain the rendered protected page with `<div>secret-data</div>`, proving the protected route was reached without authentication.
- `bundle/logs/nextjs-vuln-*.log` and `bundle/logs/nextjs-fixed-*.log`: server logs for each attempt.
- `bundle/repro/runtime_manifest.json`: runtime evidence manifest with `entrypoint_kind: api_remote` and the list of proof artifacts.
- `bundle/logs/vuln-*-build.log` and `bundle/logs/fixed-*-build.log`: build logs for each version.

Environment details:
- Node.js v24.15.0
- Next.js 14.2.24 (vulnerable) and 14.2.25 (fixed)
- React 18.3.1
- Server bound to `127.0.0.1` on ports 3000–3003

## Recommendations / Next Steps

1. **Upgrade** to the patched version: 12.3.5, 13.5.9, 14.2.25, or 15.2.3 (depending on the major release in use).
2. **Edge mitigation** until patched: configure the reverse proxy or edge layer to drop the `x-middleware-subrequest` header from all incoming client requests. Vercel already does this automatically.
3. **Do not rely solely on middleware for critical authorization** without additional defense-in-depth controls at the route or backend layer.
4. **Testing recommendations:** Add an integration test that sends `x-middleware-subrequest` with a polyglot value to every middleware-protected route and asserts that the response is still blocked (e.g., 401 or 307 to login). The Next.js upstream test suite added exactly this test in the fix commits.

## Additional Notes

- **Idempotency confirmation:** The reproduction script was run twice consecutively in a clean environment. Both runs produced identical results: vulnerable 14.2.24 allowed the bypass (200), and fixed 14.2.25 blocked it (401).
- **Claim metadata mismatch:** The bundled `ticket.json` claims `claimed_surface: library_api` and `required_entrypoint_kind: convert_document` with `expected_impact: code_execution`. This does not match the actual ticket (`ticket.md`) or the real vulnerability, which is a Next.js web-server middleware authorization bypass (`api_remote` surface) with `authz_bypass` impact. The reproduction and verdict therefore treat the actual vulnerability as confirmed at the `api_remote` surface while marking the claim as `partial` due to `scope_mismatch`/`impact_mismatch` with the submitted metadata.
- **Limitations:** The reproduction uses a minimal auth-cookie check. Real-world applications may enforce more complex middleware logic, but the bypass mechanism (skipping the middleware entirely) is independent of the specific middleware implementation.
