Now booking Q3 2026 red team engagements — limited operator availability. Request a slot →
← All writeups

CVE-2025-29927: one header turned off Next.js auth

For years, thousands of Next.js applications put their authentication check in one place: middleware. In March 2025 it emerged that any attacker could skip that check by adding a single HTTP header to their request. No credentials, no token forgery, no race condition—just one header, and every protected route opened up.

CVE
CVE-2025-29927
Class
Authorization bypass (CWE-285)
Severity
Critical — CVSS 9.1
Affected
11.1.4–12.3.4, 13.0.0–13.5.8, 14.0.0–14.2.24, 15.0.0–15.2.2 (self-hosted apps using middleware)
Patched in
15.2.3, 14.2.25, 13.5.9, 12.3.5 (no patch for 11.x)
Attack cost
One HTTP header. No authentication required.

The bug in one sentence

Next.js middleware skipped itself when it saw an internal header named x-middleware-subrequest—and that header was accepted from the outside world.

Why the code looked reasonable

This is not a case of an obviously bad line that reviewers missed. The logic existed to solve a real problem. Middleware in Next.js can trigger its own internal requests, and without a guard, middleware could invoke itself recursively until the process died. The framework's solution was a marker: when the framework makes an internal subrequest, it tags it with a header, and middleware seeing that tag knows to step aside rather than run again.

Simplified, the logic came down to something like this:

Simplified illustration of the vulnerable logic
// Read the "this is an internal subrequest" marker from the request
const subreq = request.headers['x-middleware-subrequest'];
const subrequests = typeof subreq === 'string' ? subreq.split(':') : [];

// If this middleware is named in the marker, skip running it
if (subrequests.includes(middlewareName)) {
  return NextResponse.next();  // <-- auth check never runs
}

Read that as a developer and it is a loop guard. Read it as an attacker and you ask a different question: where does that header come from? The code trusts the header's presence as proof that the framework itself created the request. Nothing verified that claim. Nothing stripped the header at the edge. An HTTP header is attacker-controlled input, and this one was being used as an authorization decision.

The pattern to remember: "internal-only" markers that travel in user-controllable channels are not internal. If a header, cookie, or query parameter can be set by a client, it cannot be evidence of trust—no matter how private the name sounds.

How the bypass worked

Exploitation was as simple as vulnerabilities get. An attacker sends a normal request to a protected route and adds the header, with a value naming the middleware to skip:

Conceptual request shape
GET /admin/dashboard HTTP/1.1
Host: target.example
x-middleware-subrequest: middleware

The exact value differed across releases, because it depended on how the framework named and counted middleware internally. Versions before 12.2 keyed off path-style names such as pages/_middleware. From 12.2 through 14.1, a single middleware or src/middleware token was enough. From 14.2 onward the framework counted repetitions against a recursion-depth limit of five, so the name had to appear five times—which is why the repeated form middleware:middleware:middleware:middleware:middleware circulated as a universal payload covering every release from 12.2 on. The variation mattered for reliable exploitation, but the principle stayed identical across every affected release: state the magic value, and the gate steps aside.

Why this one mattered more than most

Plenty of critical CVEs are hard to reach—they need a specific configuration, an authenticated session, or a chain of preconditions. This one inverted that:

  • Middleware is where auth lives. Next.js documentation and years of community convention pushed developers toward middleware as the natural place for session checks and route protection. The bug hit exactly the layer people trusted most.
  • Pre-authentication. Exploitation required no account, no token, and no user interaction—a single unauthenticated request.
  • Enormous blast radius. Next.js is one of the most widely deployed React frameworks in production, and affected releases spanned several years of versions.
  • Trivial to weaponize. Working proof-of-concept requests circulated within a day of disclosure. There is no exploit development curve on a header.

One nuance worth stating plainly, because it shaped who was actually at risk: applications hosted on Vercel, Netlify, or Cloudflare Workers were never affected—not because a filter caught the header, but because those platforms run Next.js routing in a system decoupled from the application server, so the vulnerable code path was never reachable. (Vercel did add a firewall rule blocking the header at disclosure, but that was defense in depth, not the reason its customers were safe.) Applications that never used middleware, and static exports with no server runtime, had nothing to bypass. The population truly exposed was self-hosted Next.js applications—running next start or output: 'standalone'—that relied on middleware as the authorization gate, which is still an enormous number of real applications.

What to do about it

  1. Patch. Upgrade to 15.2.3, 14.2.25, 13.5.9, or 12.3.5—or a later release within that same major line. Moving up a major line does not carry the fix on its own: 13.0.0 is newer than 12.3.5 and still vulnerable. The patch binds the skip signal to a random per-process ID and strips x-middleware-subrequest from any request that does not carry a matching one. Note that 11.x is end-of-life and never received a patch—those deployments must move to 12.3.5 or later, or block the header upstream.
  2. Strip the header at the edge. If you cannot patch immediately, drop x-middleware-subrequest at your reverse proxy, CDN, or WAF before it reaches the application. This is the vendor's own recommended workaround—but test it in monitor mode first. Cloudflare made its managed rule for this CVE opt-in after sites using third-party auth vendors saw legitimate requests start failing, and Akamai shipped its rapid rule with a default action of Alert rather than Deny for the same reason.
  3. Hunt for exploitation. Search access logs for requests carrying that header. It is the highest-fidelity single indicator available for this bug, though not a zero-false-positive one—a small number of legitimate integrations do send it—so alert and triage rather than assuming every hit is an attack.
  4. Stop terminating authorization at the edge. The deepest fix is architectural: enforce authorization again inside the route handler, server action, or data layer. Middleware is excellent for redirects and coarse routing decisions; when it is the only thing standing between an anonymous request and your data, one framework bug becomes a total compromise.

The lesson: read the code, not just the checklist

No scanner had a signature for this before disclosure. No OWASP Top 10 category announces "this framework trusts one of its own headers." It was found the way real findings are found—by reading source code and asking where a value comes from and who is allowed to influence it.

That is the same question that separates a real penetration test from an automated scan, and it is the reason we built LearnCodeReview: knowing that "authorization bypass" is a vulnerability class does not help you find one. Recognizing trust boundaries in code you are reading for the first time does.

Is your auth actually where you think it is?

Framework bugs are one way single-layer authorization fails—there are many others, and we find them by hand. Tell us your stack and we'll respond within 24 hours.

All communications encrypted · NDA available · Free retest included · Response within 24h