Feb 21, 2026·6 min read·23 visits
Next.js implemented a loop-prevention mechanism that trusted a specific header to track middleware recursion. Attackers can manually inject this header (`x-middleware-subrequest`) to convince the server that the limit has been reached, causing Next.js to skip security middleware and serve protected content.
A critical authorization bypass vulnerability in Next.js allows attackers to skip middleware execution entirely. By spoofing an internal recursion-tracking header, attackers can trick the server into believing a request has already been processed multiple times, causing it to bypass authentication checks and access protected routes directly. This affects all versions from 11.1.4 up to 15.2.2.
Middleware in Next.js is the unsung hero of modern React applications. It sits at the edge, acting as the bouncer for your routes. It checks if you have a session token, validates your permissions, rewrites paths based on your locale, and injects security headers like CSP. It is the gatekeeper. If the middleware doesn't run, the gate is effectively left wide open.
But middleware has a problem: recursion. Since middleware can rewrite requests to other paths which might trigger more middleware, there is a risk of creating an infinite loop that crashes the server (or at least the Vercel function, costing you a fortune in compute time). To solve this, Next.js needs a way to track how deep the rabbit hole goes.
The engineers implemented a solution that is logical on paper but catastrophic in practice: they used an HTTP header to count the loops. Specifically, x-middleware-subrequest. The server reads this header, counts the occurrences of the middleware name, and if it hits a hardcoded limit (usually 5), it assumes a loop is happening and... stops executing the middleware to save the process.
Here is the kicker: they trusted the client to provide the truth. They assumed that this header would only ever be present if the server put it there during an internal rewrite. They forgot the first rule of web security: Input is evil. If an attacker simply sends this header in their initial request, the server throws its hands up, says 'Whoa, too many loops!', skips the security checks, and hands over the data.
The vulnerability stems from a classic logic flaw: relying on client-controlled data for server-side state management without validation. In versions of Next.js ranging from v12.2 to v15.2.2, the logic for handling the x-middleware-subrequest header shifted from a simple path check to a recursion depth check. The code looks for a colon-separated list of middleware names in the header.
Technically, the server parses x-middleware-subrequest. It splits the string by colons and counts how many times the current middleware's name appears. If that count exceeds MAX_RECURSION_DEPTH (which is set to 5), the runMiddleware function essentially returns early, passing the request downstream to the page handler or API route without running the user-defined logic in middleware.ts.
This is the architectural equivalent of a bank vault that locks automatically at 5:00 PM, but checks the time by looking at the customer's wristwatch. If you walk in at noon and tell the teller 'It's 5:01 PM', they unlock the vault and go home, leaving you inside. Because the header is processed before any user code runs, there is nothing a developer can write in their own middleware.ts to stop this. The framework fails before your code even loads.
Let's look at the logic that caused the issue. While the exact file paths vary by version, the core logic in the adapter.ts or next-server components effectively did this:
// Pseudocode of the vulnerable logic
const MAX_RECURSION_DEPTH = 5;
function getMiddlewareRecursion(req) {
const header = req.headers.get('x-middleware-subrequest') || '';
const depth = header.split(':').filter(n => n === middlewareName).length;
return depth;
}
// Later in the request lifecycle...
if (getMiddlewareRecursion(req) >= MAX_RECURSION_DEPTH) {
// Assume infinite loop, skip middleware execution
return { finished: true };
}The fix, introduced in commit 52a078da3884efe6501613c7834a3d02a91676d2 (and backported in 5fd3ae8f8542677c6294f32d18022731eab6fe48), introduces a shared secret. The server now generates a random token at startup. Internal sub-requests must sign the header with this token.
// The Fix: Validation of the source
if (requestHeaders.has('x-middleware-subrequest')) {
const subrequestId = requestHeaders.get('x-middleware-subrequest-id');
// Check if the secret matches what the server generated at startup
if (subrequestId !== internalSecret) {
// If it doesn't match, the client is spoofing it. Strip the header.
requestHeaders.delete('x-middleware-subrequest');
}
}This ensures that only the Next.js server itself can increment the recursion counter. If an external attacker tries to inject the header, they won't know the random 8-byte hex string (stored in a Symbol in globalThis), so the server strips their fake header and runs the middleware as normal.
Exploiting this requires zero coding skills and standard command-line tools. You don't need buffer overflows; you just need to know how to count to five.
The Scenario: You have a Next.js application protecting an admin panel at /admin. The middleware checks for a session cookie and redirects to /login if it's missing.
The Attack:
/admin. Receive 307 Temporary Redirect to /login.middleware or src/middleware.curl -v http://target.com/admin \
-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"The Result: The server sees the header, calculates depth = 5, triggers the loop prevention logic, skips the auth check, and returns 200 OK with the contents of the admin dashboard.
> [!WARNING]
> In some setups, the middleware name might include the path, like pages/_middleware. Exploitation tools like Nuclei simply spray variants: src/middleware:middleware:pages/_middleware... to cover all bases.
The immediate impact is obvious: broken access control. If you use Next.js middleware to gate content, that gate is gone. Admin panels, user dashboards, and premium content are accessible to anyone.
However, the secondary impact is potentially worse: Web Cache Poisoning.
Many Next.js sites sit behind CDNs (Vercel Edge Network, Cloudflare, Fastly). If an attacker sends the exploit request and the CDN is configured to cache the response (even briefly), the CDN might cache the bypassed version of the page (the 200 OK admin panel) instead of the redirect.
Now, legitimate administrators or users navigating to that URL will be served the cached, exploited content. Or worse, if the page renders user-specific data (PII) relying on the middleware to filter it, an attacker could force the server to generate a page with their own malicious state, cache it, and serve it to victims. This turns a targeted bypass into a broad Denial-of-Service or Information Disclosure event.
The vulnerability is patched in Next.js versions 15.2.3, 14.2.25, 13.5.9, and 12.3.5. The patch logic relies on stripping the malicious header if a corresponding secret ID is absent.
Immediate Remediation Strategy:
next dependency immediately. npm install next@latest.Mitigation (If you cannot upgrade): If you are stuck on a legacy version or cannot deploy immediately, you must implement a block at your network edge.
Nginx Configuration:
# Drop the header before it hits the node process
proxy_set_header x-middleware-subrequest "";Cloudflare WAF Rule:
Create a firewall rule blocking any request where the header x-middleware-subrequest exists. Since this header is internal-only, legitimate clients (browsers) never send it. It is safe to drop all external traffic containing it.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Next.js Vercel | 11.1.4 - 12.3.4 | 12.3.5 |
Next.js Vercel | 13.0.0 - 13.5.8 | 13.5.9 |
Next.js Vercel | 14.0.0 - 14.2.24 | 14.2.25 |
Next.js Vercel | 15.0.0 - 15.2.2 | 15.2.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-285 (Improper Authorization) |
| Attack Vector | Network (HTTP Headers) |
| CVSS | 9.1 (Critical) |
| EPSS Score | 0.92896 (92.90%) |
| Impact | Full Auth Bypass |
| Exploit Status | Active / POC Available |
The software does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.