Skip to content

The Next.js middleware bypass

Next.js trusted an internal header on requests from the internet. Any client could add it, and self-hosted apps skipped their middleware, login checks included. This is how one request walked past the gate.

  • CVE-2025-29927
  • CVSS 9.1
  • Self-hosted apps exposed
  • Not in CISA KEV

In the drawing, the request turns amber where Next.js believes its forged internal header, and walks past the middleware without stopping.

The request's path into a self-hosted Next.js app, drawn as nested layersSix nested layers recede toward the lower right: HTTP request, self-hosted Next.js, router-server, the internal header check, the auth middleware (dashed, because it never runs) and the protected route. A pale line steps inward through the first three. Where it enters the header check, marked “header trusted from outside”, it turns amber, walks straight across past the middleware without stepping into it, and drops into the protected route, which serves the page with no login.HTTP requestself-hosted Next.jsrouter-serverauth middlewareprotected routeheader trusted from outsidepage served,no login

What happened

Next.js used an internal request header, x-middleware-subrequest, to stop middleware from calling itself forever. It read that header from any request, including ones from the internet. A client that added it could make a self-hosted app skip its middleware, and every login or permission check that lived there.

Reported 27 Feb 2025 by Allam Rachid and Allam Yasser; confirmed 14 Mar; patched 17–23 Mar across four release lines; public 21 Mar. CVSS 9.1. Apps on Vercel, Netlify and Cloudflare Workers were never affected. The fix is to upgrade; the stopgap is to strip the header at the edge.1

Am I affected?

Only if the app is self-hosted (next start or output: 'standalone') and its middleware does security checks. Vulnerable: next 11.1.4 through 15.2.2, below 12.3.5, 13.5.9, 14.2.25 and 15.2.3 on each line. 11.x never got a fix. Not affected: Vercel, Netlify, Cloudflare Workers and static exports.2

A guide built from the advisories, not a scanner, and it cannot see how you deploy: the verdict assumes the app is self-hosted. Run npm ls next to see what your lockfile resolves. Try

Next.js release lines (GHSA-f82v-jwr5-mffw, GHSA-223j-4rm8-mrmf)
LineVulnerableBypass fixed inTake (CVE-2025-30218)
11.x11.1.4no fixstrip the header
12.x12.0.0 – 12.3.412.3.512.3.6
13.x13.0.0 – 13.5.813.5.913.5.10
14.x14.0.0 – 14.2.2414.2.2514.2.26
15.x15.0.0 – 15.2.215.2.315.2.4
How the app is served
DeploymentAffected?
next startYes, if middleware does security checks
output: 'standalone'Yes
Vercel · Netlify · Cloudflare WorkersNo
output: 'export'No: middleware does not run

The CVE record and Vercel's advisory start the range at 11.1.4; GitHub's global database now starts it at 12.0.0. See which versions. Sources: GHSA-f82v-jwr5-mffw, CVE.org, Vercel's postmortem.

Do this now

  1. Upgrade next to 12.3.6, 13.5.10, 14.2.26 or 15.2.4. Those carry the bypass fix and the fix for the bug the patch itself introduced (CVE-2025-30218), so you upgrade once.9
  2. Can't upgrade, or on 11.x? Drop or reject requests carrying x-middleware-subrequest at the load balancer, reverse proxy, CDN or WAF, before they reach Next.js. That is the advisory's own workaround.1
  3. Inventory how each app is served. The same version was exploitable self-hosted and safe on Vercel, Netlify or Cloudflare Workers.
  4. Check where your auth lives. If middleware is the only thing guarding a route, add a session check in the route or data layer too.8
  5. Don't stop at a WAF rule. Cloudflare's managed rule broke legitimate requests on sites using third-party auth middleware and was made opt-in within a day; Cloudflare still recommends updating.7
Contents
  1. Field marks
  2. How it works
  3. Which versions
  4. Who was exposed
  5. Timeline
  6. The fix's own bug
  7. Exploitation
  8. Where auth belongs
  9. Defenses
  10. Related
  11. Sources

An attempt is an ordinary request with one extra header.

In requests

  • Requests from outside that carry an x-middleware-subrequest header. 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 /login answering 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.
Path of a request with a forged internal header through a self-hosted Next.js serverAn HTTP client with no session sends GET /dashboard carrying an x-middleware-subrequest header it added itself. An edge proxy passes headers on as sent. Inside the self-hosted Next.js server, router-server hands the request to the middleware runner, which reads the header before running anything: up to 12.1 it matched the middleware's path name, from 12.2 the middleware file's name, and later versions skip once the name appears five times. When it matched, the runner skipped middleware.ts, whose job was to redirect requests without a session to /login, and the /dashboard route ran as if middleware had allowed it, answering 200 OK with the protected page. The header exists for middleware's own subrequests, which Next.js marks so middleware cannot call itself forever. The fix adds a filter in router-server that drops the header unless a per-process secret id matches; the stopgap strips the header at the edge.HTTP clientbrowser or any scriptno account, no session cookieGET/dashboarda protected pagecookienone: not logged inx-middleware-subrequestadded by the clientEdge or proxyload balancer · CDN · WAFpasses headers on as sentstrips the headerthe stopgapSelf-hosted Next.js serverrouter-serverevery request enters herenext start · output: standaloneMiddleware runnerreads x-middleware-subrequest before it runs anythingrunMiddlewareUp to 12.1matches the path name12.2 onwardmatches the file nameLater versionsname seen 5 times: skipno match: run itmiddleware.tsno session cookie?redirect to /loginon the bypass: never runsSubrequestmiddleware fetches the appNext.js adds the header to mark itfilterInternalHeadersunless its secret id matchesmatched: skip/dashboardpage · API route · rewrite targetruns as if middleware had saidNextResponse.next()200 OKthe protected page, served with no loginunless the route checks the session itself, close to the data
Diagram of the middleware-bypass request path. Focus it and use the left and right arrow keys to move between the eight steps listed after it; each step's text describes that part in full.

  1. 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
  2. Middleware is the app's gatekeeper.

    The app's middleware.ts (or pages/_middleware before 12.2) matches the path and runs first. Its job here: read the session cookie, and redirect to /login if 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
  3. Middleware can call the app, so Next.js marks its own requests.

    Middleware may fetch the same app, and that request passes through middleware again. To stop it calling itself forever, Next.js tracks re-entry with x-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
  4. The header is read before the middleware runs.

    In the server's runMiddleware path, Next.js reads the header from the incoming request first. The test changed over the years. Up to 12.1, with pages/_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).15

    The 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
  5. 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
  6. 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
  7. 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
  8. 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. In filterInternalHeaders, any request whose id does not match has x-middleware-subrequest deleted before the check sees it. A regression test confirms a request carrying the header is still redirected by middleware.5

    Until 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-subrequest header is passed; filter it out if validation fails.

Vercel, “Postmortem on Next.js middleware bypass”, 25 Mar 2025

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

Affected and fixed Next.js versions per release line11.x: 11.1.4 is affected and no fix was released; the workaround is to strip the header. 12.x: 12.0.0 to 12.3.4 vulnerable, fixed in 12.3.5, follow-up fix 12.3.6. 13.x: 13.0.0 to 13.5.8, fixed in 13.5.9, follow-up 13.5.10. 14.x: 14.0.0 to 14.2.24, fixed in 14.2.25, follow-up 14.2.26. 15.x: 15.0.0 to 15.2.2, fixed in 15.2.3, follow-up 15.2.4.LineVulnerableCVE-2025-29927 fixedTake this: also fixes CVE-2025-3021811.x11.1.4 · the last 11.x releaseno fix releasedworkaround: strip the header12.x12.0.0 – 12.3.412.3.512.3.613.x13.0.0 – 13.5.813.5.913.5.1014.x14.0.0 – 14.2.2414.2.2514.2.2615.x15.0.0 – 15.2.215.2.315.2.4Segments are not to scale. Where the range starts is disputed: the CVE record says 11.1.4, GitHub's global database now says 12.0.0.
Fig. 1. Release lines, first fix and the fix to take. The right-hand column also closes CVE-2025-30218. 11.x has no fixed release; the advisory's answer there is to strip the header at the edge. Sources: GHSA-f82v-jwr5-mffw, GHSA-223j-4rm8-mrmf, CVE.org, the npm registry.

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

Who CVE-2025-29927 affected, by how the app is servedExposed: self-hosted apps run with next start or built with standalone output, because Next.js's own server reads the header and skips middleware. Not affected: Vercel, which was automatically protected; Netlify and Cloudflare Workers, which run middleware separately from application routing; and static exports, which run no middleware. Apps that re-check authentication in the route or data layer were not exposed in practice.How the app is servedCVE-2025-29927Whyself-hosted, next startExposedNext.js's own server reads the header and skips middlewareself-hosted, standalone outputExposedalso Next.js's own server: the same exposureVercelNot affected“automatically protected” (the advisory)NetlifyNot affectedmiddleware runs apart from routing; adapter tests addedCloudflare WorkersNot affectedthe same “decoupling of application routing”static exportNot affectedoutput: 'export' runs no middleware at allauth re-checked in the routeNot exposedthe route refuses the request itself
Fig. 2. Who was affected. The deployment shape is part of the attack surface. Sources: Vercel's postmortem and advisory, Netlify, Datadog.

[Netlify sites] are not and have never been affected.

Netlify, “Next.js sites on Netlify are not vulnerable to CVE-2025-29927”, 22 Mar 2025

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.

CVE-2025-29927 timeline, 27 February to 8 April 2025Reported 27 February naming Next.js 12.x; a follow-up on 1 March said newer versions were affected too; Vercel began investigating on 5 March and the Next.js team confirmed the issue on 14 March, fifteen days after the report. The CVE ID was reserved on 12 March. 14.2.25 and 15.2.3 shipped on 17 and 18 March. The CVE and advisory went public on 21 March. 13.5.9 and 12.3.5 followed on 22 and 23 March, when ProjectDiscovery also published a detection template. The follow-up fixes 12.3.6, 13.5.10, 14.2.26 and 15.2.4 shipped on 24 March; Vercel's postmortem came on 25 March; Datadog reported low scanning volume on 28 March; CVE-2025-30218 was published on 2 April; and on 8 April CISA recorded exploitation as none.15 days from report to confirmationFeb 27Mar 7Mar 14Mar 21Apr 4Reported, naming Next.js 12.xFollow-up: newer versions affected tooVercel starts investigatingNext.js confirmsFixed: 14.2.25, 15.2.3CVE and advisory publicBackports: 13.5.9, 12.3.5Follow-up fixes: 12.3.6 · 13.5.10 · 14.2.26 · 15.2.4CISA: exploitation “none”CVE ID reservedDetection template (ProjectDiscovery)Vercel postmortemMar 28 · Datadog: low scanning volumeCVE-2025-30218 (low)
Fig. 3. Report to follow-up fix, 2025. Drawn to scale in days. Amber marks the report-to-confirmation gap, the confirmation and the public disclosure. Sources: Vercel's postmortem, the researchers' write-up, CVE.org, the npm registry, Cloudflare, ProjectDiscovery, Datadog, CISA.

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.

Next.js, “Authentication”, docs, updated 25 Aug 2026
  • 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.

ControlEffect hereWhy
Upgrade to the latest patch on your lineStops it12.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 edgeStopgapThe 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 dataMakes it mootA 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 routingNot exposedVercel, 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 rulesBluntCloudflare 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 actionsDoesn't applyThese 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-subrequest header from reaching your Next.js application.

Vercel, “Authorization Bypass in Next.js Middleware”, 21 Mar 2025

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

Primary · advisory databases, registries and government

Secondary · discoverers and researchers

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.