Contents
An attempt is an ordinary request with one extra header.
In requests
- Requests from outside that carry an
x-middleware-subrequestheader. The advisory treats any such external request as one to stop at the edge.1 - Not every one is hostile: when Cloudflare blocked the header for everyone, sites using third-party auth middleware saw legitimate requests fail, and the rule became opt-in.7
- ProjectDiscovery published a Nuclei detection template two days after disclosure, and Datadog shipped a WAF detection rule: the probe is cheap to run and easy to see.1516
On an app that was hit
- A route that middleware should have redirected to
/loginanswering a request that carried no session at all: admin pages, dashboards, paywalled content and API routes are the advisory's examples. - Nothing in middleware's own logs for that request, because middleware never ran.
Why the EPSS score is near the maximum
GitHub's advisory shows an EPSS score of 0.99, in the 99.93rd percentile. That reflects how easy and automatable the check is, not observed compromise: CISA lists exploitation as “none” and Datadog saw “a relatively low number of scans”.
One header let a request walk past the gate.
The bug sat where an internal control signal and untrusted client data travelled in the same channel: HTTP request headers. Next.js wrote the header for its own subrequests, then believed it on every request, whoever had written it. The safety check meant to stop a loop failed open, and skipped the gatekeeper.
Scroll, or focus the stage and use the arrow keys. The amber dot is the request; the camera moves to the part of the server each step is about. The header is named, never filled in: there is no working request here.
- MiddlewareA function that runs before a request reaches a page or API route. Current Next.js docs call it Proxy.
- Confused deputyA trusted component tricked into acting on a request's say-so: here, the server believing a client's claim to be its own subrequest.
A request for a protected page, with no session.
A client asks a self-hosted Next.js server for a protected path such as
/dashboard. It has no account and sends no session cookie. On a normal day, that request should end at the login page.This one carries one extra header,
x-middleware-subrequest, which the client added itself. Nothing else about it is unusual: one ordinary HTTP request, no user interaction.- path
- /dashboard
- session cookie
- none
- extra header
- x-middleware-subrequest
- login needed
- no
Middleware is the app's gatekeeper.
The app's
middleware.ts(orpages/_middlewarebefore 12.2) matches the path and runs first. Its job here: read the session cookie, and redirect to/loginif it is missing. Only if it lets the request through does the route run.Many apps used it exactly this way, as the one place their auth check lived.17
- runs
- before the route
- no session
- redirect to /login
- session
- on to the route
- often
- the only auth check
Middleware can call the app, so Next.js marks its own requests.
Middleware may
fetchthe same app, and that request passes through middleware again. To stop it calling itself forever, Next.js tracks re-entry withx-middleware-subrequest: a list of the middleware names that have already run on this chain of requests.The header was meant to be written by Next.js alone, on subrequests it made itself.16
- purpose
- stop infinite loops
- header
- x-middleware-subrequest
- holds
- middleware names already run
- meant to be set by
- Next.js only
The header is read before the middleware runs.
In the server's
runMiddlewarepath, Next.js reads the header from the incoming request first. The test changed over the years. Up to 12.1, withpages/_middleware, it compared the header against the middleware's file-path name, so it depended on route nesting. From 12.2 the name is simply the middleware file's. Later versions count instead: the skip triggers once the name appears five times (MAX_RECURSION_DEPTH).15The guard changed three times. The trust it placed in the header never did.
- where
- runMiddleware
- up to 12.1
- matches the path name
- 12.2 onward
- matches the file name
- later versions
- skip after 5 matches
- read from
- the incoming request
Nothing told Next.js's header apart from the client's.
Nothing stripped the header from requests arriving from outside, and nothing proved where it came from. A load balancer or CDN in front passed it on like any other header. To the server, a client that wrote the header looked exactly like an internal subrequest.
That is a confused deputy: a security decision made on input the client controls. The weakness catalogue calls the outcome improper authorization (CWE-285).11
- written by
- the client
- stripped at the edge
- no
- authenticated
- no
- CWE
- 285 · 863
The check matched, so middleware was treated as done.
When the header matched, Next.js decided the middleware had already run on this chain and moved on as if it had returned
NextResponse.next(), meaning “carry on”. The middleware function was never called.The guard existed for performance and loop prevention. Its failure mode was to skip the gatekeeper entirely: it failed open.15
- header matched
- yes
- middleware.ts
- never called
- treated as
- NextResponse.next()
- failure mode
- fail open
The route runs unguarded and serves the page.
The request reaches the page, API route or rewrite target with no redirect and no auth check. If the route itself did not verify the session, it serves the protected content: admin pages, dashboards, paywalled content, API routes.1
Apps that checked the session again next to the data were not exposed: the route refused the request itself.6
- redirect
- none
- auth check
- never ran
- response
- 200 OK, protected content
- safe if
- the route checks the session
The fix: the header counts only with a secret only the server knows.
Commit 52a078da (#77201) makes the server generate 8 random bytes at startup and keep them as a per-process secret. Genuine middleware subrequests carry it as
x-middleware-subrequest-id. InfilterInternalHeaders, any request whose id does not match hasx-middleware-subrequestdeleted before the check sees it. A regression test confirms a request carrying the header is still redirected by middleware.5Until you can upgrade, the edge can do the same job bluntly: strip the header before it reaches Next.js.
- secret
- 8 random bytes per process
- carried as
- x-middleware-subrequest-id
- no match
- header deleted
- stopgap
- strip it at the edge
The numbered markers on the diagram match the steps.
Add validation when the
x-middleware-subrequestheader is passed; filter it out if validation fails.
Every line from 11.1.4 to 15.2.2, though the databases disagree on where it starts.
Four release lines got a fix: 12.3.5, 13.5.9, 14.2.25 and 15.2.3. A week later each got a second release, 12.3.6, 13.5.10, 14.2.26 and 15.2.4, for a bug the patch itself introduced. Take the second set and you upgrade once.9
Where the range starts depends on which record you read sources disagree. The CVE record and Vercel's own advisory say >= 11.1.4, < 12.3.5 for the first range, and tell 11.x users to use the workaround.10 GitHub's global advisory database, updated 2 March 2026, now starts that range at >= 12.0.0. NVD's description says “1.11.4”, a typo for 11.1.4.11
The npm registry explains the oddity. 11.1.4 was published on 27 January 2022, after 12.0.0 (26 October 2021): a late 11.x backport, and the last stable 11.x release. By version number, “11.1.4 through 15.2.2” is accurate, and there was never a patched 11.x. The researchers also say every version from 11.1.4 on was affected.12
The same version was exploitable self-hosted and safe on Vercel.
Whether an app was exposed depended less on its next version than on how it was served. Self-hosted apps, run with next start or built with output: 'standalone', were affected if their middleware did security checks. Vercel, Netlify and Cloudflare Workers run middleware separately from application routing, so an incoming header never reached the skip logic. Static exports run no middleware at all.2
[Netlify sites] are not and have never been affected.
The postmortem uses the same words for Cloudflare Workers, which “never have been affected, for the same decoupling of application routing”. Netlify added end-to-end tests for it to the OpenNext Netlify adapter.6
Fifteen days to confirm. Then every line patched within a week.
- 15days
from the report (27 Feb) to the Next.js team confirming it (14 Mar).
- ~5days
from the 15.x patch (18 Mar) to the 12.x patch (23 Mar). 11.x got none.
- 7days
from the first patch (17 Mar) to the follow-up releases (24 Mar).
- 9.1
CVSS: network, low complexity, no privileges, no user interaction.
The first report, on 27 February, described Next.js 12.x and was treated as lower priority; the researchers say they then believed only 12.0.0–12.0.7 were affected. A follow-up on 1 March said newer versions were affected too, and Vercel says triage was delayed. Its postmortem lists three lessons of its own: slow triage, too little proactive outreach to infrastructure partners, and confusing messaging about Vercel's own protection.14
The fix leaked its own secret, and needed a fix a week later.
The patch's new x-middleware-subrequest-id header was attached to every fetch middleware made, including requests to third-party hosts. That sent the per-process secret to outside servers. It became CVE-2025-30218 (GHSA-223j-4rm8-mrmf), rated low, fixed in 12.3.6, 13.5.10, 14.2.26 and 15.2.4 on 24 March and published on 2 April.4
Vercel said it was “already planning on removing this recursion prevention logic from Middleware”, which the newer Node.js-runtime middleware did not support, and that the disclosure expedited the change.
Take the latest patch on your line
Upgrading to the first fixed version meant upgrading twice. The releases from 24 March close both CVEs, which is why every recommendation on this page names them.
Trivial to test, and little sign of it in the wild.
- CISA: not in the Known Exploited Vulnerabilities catalog (feed checked 1 October 2026). Its SSVC assessment on 8 April 2025 records exploitation as “none”, automatable “yes” and technical impact “total”.13
- Datadog (28 March 2025): “a relatively low number of scans”, from 34 IPs across at least five environments, “generally small in volume, ineffective, and poorly targeted”.16
- ProjectDiscovery published a Nuclei detection template on 23 March, so checking a site for the flaw became trivial.15
The impact the advisory names is unauthenticated access to any route whose only protection was middleware. With no login, no user interaction and a single request, the bar was low.
Middleware is a convenience layer, not the lock.
Apps that checked the session again next to their data, in route handlers, Server Actions or a data access layer, were not exposed in practice: skipping middleware skipped a redirect, not the check that mattered. Next.js's current guidance, where middleware is now called Proxy, says it “should not be your only line of defense”.8
The majority of security checks should be performed as close as possible to your data source.
- Data access layerOne place in the server code where every read of protected data checks who is asking. A skipped middleware cannot skip it.
- Fail openWhen a check's failure lets the request through instead of stopping it. The recursion guard failed open.
Upgrading or stripping the header stopped it. Checking auth near the data made it moot.
Which controls would have stopped or limited this one. Like any bug in trusted code, the supply-chain controls built for malicious packages do not apply.
| Control | Effect here | Why |
|---|---|---|
| Upgrade to the latest patch on your line | Stops it | 12.3.6, 13.5.10, 14.2.26 or 15.2.4 close the bypass and the patch's own follow-up. 11.x has no fix. |
| Strip the header at the edge | Stopgap | The advisory's workaround: drop or reject x-middleware-subrequest on external requests at the load balancer, proxy, CDN or WAF. It only holds if every path to the app goes through it (step 8). |
| Auth re-checked next to the data | Makes it moot | A route or data layer that checks the session refuses the request even when middleware never ran. Apps built this way were not exposed. |
| Hosting that runs middleware apart from routing | Not exposed | Vercel, Netlify and Cloudflare Workers were never affected. That is a property of the platform, not a control aimed at this bug: upgrade anyway. |
| Managed WAF rules | Blunt | Cloudflare deployed a rule to all sites on 21 March, then made it opt-in on 22 March because it broke legitimate third-party auth middleware. Datadog shipped a detection rule. |
| Minimum release age, provenance, 2FA, pinned actions | Doesn't apply | These guard against malicious publishes. Here the code was published honestly by its maintainers; the bug was in it. |
…prevent external user requests which contain the
x-middleware-subrequestheader from reaching your Next.js application.
Sources
Primary sources are the vendors, hosting platforms, advisory databases, registries and governments responsible for the facts. Secondary sources are the discoverers' and security firms' own analyses.
Primary · vendors and hosting platforms
- 1Vercel (Next.js)Authorization Bypass in Next.js Middleware (GHSA-f82v-jwr5-mffw)
Description, the >= 11.1.4 range, the 11.x workaround, impact, CVSS, CWE-285, credits, Vercel automatically protected.
- 2Vercel (Ty Sbano)Postmortem on Next.js middleware bypass
Full timeline, affected and unaffected deployments, the fix's concept, communication lessons. nextjs.org/blog/cve-2025-29927 redirects here.
- 3VercelVercel Firewall proactively protects against vulnerability with middleware
Vercel customers unaffected.
- 4VercelCVE-2025-30218
The follow-up leak of x-middleware-subrequest-id and the plan to remove the recursion logic.
- 5Next.jscommit 52a078da (#77201) and commit 5fd3ae8f (14.x backport)
The per-process secret, the header filter and the regression test described in step 8.
- 6NetlifyNext.js sites on Netlify are not vulnerable to CVE-2025-29927
Netlify never affected; end-to-end tests added to the adapter; apps that validate later were not affected.
- 7CloudflareNext.js vulnerability WAF rule
The managed rule, why it became opt-in, the follow-up versions.
- 8Next.jsAuthentication guide
Proxy (formerly middleware) is not the only line of defense; checks close to the data.
Primary · advisory databases, registries and government
- 9GitHub Advisory DBGHSA-f82v-jwr5-mffw and GHSA-223j-4rm8-mrmf (CVE-2025-30218)
The >= 12.0.0 range in the global database, CWE-285 and CWE-863, EPSS; the follow-up's fixed versions.
- 10CVE ProgramCVE-2025-29927
CNA, reservation and publication dates, the >= 11.1.4 range, CISA's SSVC assessment.
- 11NIST NVDCVE-2025-29927
The “1.11.4” typo, CWE-863 as primary, the CNA's CVSS score.
- 12npm and GitHubnext on the npm registry and Next.js releases
Publish times of the fixed versions; 11.1.4 published 2022-01-27, the last 11.x.
- 13CISA
Secondary · discoverers and researchers
- 14Rachid Allam and Yasser AllamNext.js and the corrupt middleware
The discovery, the version-specific behaviour, 11.1.4 onward, their side of the timeline.
- 15ProjectDiscoveryNext.js middleware authorization bypass
The runMiddleware check, the version notes, the Nuclei template, stripping at the edge.
- 16Datadog Security LabsNext.js middleware auth bypass
The mechanism, unaffected deployments, low scanning volume.
- 17oss-security (Alan Coopersmith)oss-security post
Affected deployments, patched versions, mitigation.
Written from sources checked on 2026-10-01. Every fact above traces to one of them; claims that could not be confirmed on a primary page are left out. Pull quotes are verbatim from the linked primary pages; the bracketed words in Netlify's are ours, and the ellipsis in the advisory's marks where its sentence begins.