Safeguard
Vulnerability Management

CVE-2025-29927: inside the Next.js middleware auth bypass

A single spoofed header let attackers skip Next.js middleware entirely — CVSS 9.1, four major versions affected, exploited in the wild within days.

Safeguard Research Team
Research
6 min read

On March 21, 2025, the Next.js team disclosed CVE-2025-29927, a CVSS 3.1 score of 9.1 (critical) vulnerability that let an attacker skip authorization checks implemented in middleware simply by sending a request with a specific internal header attached. The bug, tracked upstream as GHSA-f82v-jwr5-mffw and filed under CWE-863 (Incorrect Authorization) and CWE-285 (Improper Authorization), affected every actively maintained release line: versions from 11.1.4 up to but not including 12.3.5, 13.0.0 up to 13.5.9, 14.0.0 up to 14.2.25, and 15.0.0 up to 15.2.3. Fixed releases landed the same day at 12.3.5, 13.5.9, 14.2.25, and 15.2.3. The vector string — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N — describes a network-reachable flaw requiring no privileges, no user interaction, and low attacker effort, with high confidentiality and integrity impact. Because so many production apps route authentication and access-control logic through a single middleware.ts file, the practical effect was that a crafted request could reach an admin dashboard, an internal API route, or any "protected" page as if it were public. Scanning activity targeting the vulnerable header was observed in the wild within days of disclosure, and the header used to trigger it — x-middleware-subrequest — is now something every Next.js operator needs to understand.

What is the x-middleware-subrequest header supposed to do?

x-middleware-subrequest is an internal signaling header Next.js uses to prevent middleware from recursively invoking itself. When middleware issues an internal subrequest — for example, rewriting a request in a way that would otherwise re-trigger the middleware chain — the framework tags that follow-up request so its own routing layer recognizes "this has already passed through middleware, don't run it again." It's a loop-prevention mechanism, not a security boundary, and it was never designed to be set by anyone outside the framework's own internal request handling. The header's expected value is an internal marker (the exact format varies by version and includes information Next.js uses to match it against the current middleware path), so it isn't something a legitimate client request should ever carry. That distinction — internal plumbing versus untrusted external input — is exactly what CVE-2025-29927 broke: nothing on the server validated that the header could only originate from Next.js itself.

How did a spoofed header bypass authorization entirely?

Because Next.js trusted the x-middleware-subrequest header on any incoming request, an external attacker could set it directly in an HTTP request and the framework would treat that request as an internal subrequest that had already run through middleware — and skip executing middleware a second time. If your application's authorization logic lived entirely inside middleware.ts (checking a session cookie, verifying a JWT, or redirecting unauthenticated users to /login), that logic simply never ran. The request fell straight through to the route handler, page, or API endpoint underneath, with none of the gatekeeping the developer assumed was in place. No credential theft, no session hijack, no injection payload was required — just one extra header on an otherwise ordinary GET or POST request. This is what pushed the severity to critical: the attack works pre-authentication, needs no user interaction, and is trivially scriptable, which is consistent with the vulnerability being scanned and exploited in the wild shortly after public disclosure.

Why did middleware-only authorization become such a common pattern?

Next.js middleware runs at the edge, before a request resolves to a specific route, which made it an attractive single choke point for auth: one file to gate an entire /admin tree or a whole set of dashboard routes, instead of repeating a session check in every page and API handler. Framework documentation and countless tutorials reinforced this pattern because it's genuinely convenient — write the check once, apply it broadly. The problem is architectural, not just this one bug: middleware is a routing-layer construct, and routing-layer constructs are exactly the layer attackers try to route around. Any mechanism that lets a request skip or reorder how middleware executes — a header, a rewrite quirk, a caching edge case — collapses every downstream assumption at once. CVE-2025-29927 is the sharpest illustration of that risk, but it's the general pattern (a single, bypassable enforcement point protecting many resources) that security teams should treat as the real lesson, independent of this specific header.

What does the official mitigation actually require?

The most direct fix is upgrading to a patched release — 12.3.5, 13.5.9, 14.2.25, or 15.2.3 and later — since the framework itself now rejects or ignores externally-supplied x-middleware-subrequest headers. Where an immediate upgrade isn't possible, NVD's listed mitigation is to block external requests carrying the x-middleware-subrequest header before they reach the Next.js application — strip or reject it at a reverse proxy, load balancer, CDN edge rule, or WAF, so only Next.js's own internal traffic can ever carry it. Neither option is a substitute for the other during a rollout: patching closes the framework-level trust gap, while an edge-level header block gives you an immediate compensating control while you schedule the version bump across every affected service. Given how many teams run several Next.js apps on different release cadences, treating the header block as a fleet-wide network rule — not a per-app config change — closes the gap faster than waiting on individual deploy pipelines.

What's the deeper defense-in-depth lesson here?

The durable fix isn't just patching Next.js — it's not putting all your authorization eggs in one enforcement layer. Middleware is a legitimate place to short-circuit unauthenticated traffic early for performance and UX, but the actual authorization decision — does this identity have permission to access this resource — should be checked again at the route handler, API layer, or server action that does the real work. That way, if any future bug, misconfiguration, or edge-case routing behavior lets a request slip past the outer layer, the inner check still stops it. This is the same principle that shows up across unrelated stacks: an API gateway that trusts a header from "internal" services, a CDN that assumes upstream already validated auth, a mobile backend that checks a token once at the edge and never again downstream. CVE-2025-29927 is a Next.js-specific bug with a Next.js-specific fix, but the architectural takeaway — verify authorization at the resource, not just at the perimeter — applies well beyond any one framework.

How Safeguard helps

Safeguard's SCA engine resolves your full dependency graph — including a pinned next version in package.json and its lockfile — and matches it against CVE/GHSA advisories enriched with CVSS and CISA KEV status, so a vulnerable Next.js version surfaces with severity context and any known-exploited signal attached, rather than as an anonymous line in an audit report. For teams running Next.js in production, Safeguard's DAST engine tests running, verified targets for missing or misconfigured security headers and runtime misconfigurations, which is the class of check relevant to confirming that an edge-level block on internal-only headers like x-middleware-subrequest is actually in effect rather than assumed. Both engines land findings in the same tenant-scoped model, so a flagged dependency and a runtime configuration gap on the same application are visible side by side instead of living in separate reports.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.