Next.js used x-middleware-subrequest to tell itself that a request was its
own internal subrequest, so middleware should not run again. An external client
could send the same header. A request carrying it skipped middleware, and any
authorisation check living there was skipped with it
(GitHub Advisory,
published 21 March).
Patched releases are 12.3.5, 13.5.9, 14.2.25 and 15.2.3. There is no fixed 11.x, so an application on 11.x has to move major version or live on the workaround.
The bug is a trust boundary in the wrong place¶
An internal marker is only internal while nothing outside can produce it. The moment a header travels over the same channel as user input, it is user input, whatever the framework calls it.
That is a very old class of mistake wearing modern clothes. It keeps happening because the component drawing the boundary and the component enforcing it are the same piece of software. Middleware decides whether to run, based on a header the request carries. Nothing above it is adjudicating.
Which is also why the mitigation works at all.
Strip it at the proxy¶
The advisory's guidance for anyone who cannot upgrade is to stop external requests carrying that header from reaching the application. At an nginx reverse proxy that is one directive:
proxy_set_header x-middleware-subrequest "";
The proxy is above the application, so it can assert something the application
cannot assert about itself. The header is now unforgeable from outside because
the only party who can set it is the one component that was ever entitled to.
Putting the check one layer up is the real repair; upgrading next is how you
stop needing it.
Our role is at
github.com/axisops/CVE-2025-29927,
MIT. It finds Next.js applications, classifies them against the advisory's
ranges and with --apply writes that snippet into every server block of the
nginx files you list.
Two things about nginx that cost an afternoon¶
proxy_set_header does not inherit into a block that defines its own. If a
location sets any proxy_set_header of its own, it inherits none from the
server level, so a snippet installed only at server silently does not apply
there. That is standard nginx inheritance, and it is just the shape of failure that produces a mitigation everyone believes is deployed. The role adds
the snippet to those location blocks too.
Test before reload, and roll back on failure. The role runs nginx -t
after writing and reverts every change if the test fails; the Ansible handler
reloads only once the test has passed. A config error here does more than degrade one application: it takes down every site on the proxy, which is a worse outcome
than the vulnerability being mitigated.
Version detection reads node_modules/next/package.json first, then falls back
to package-lock.json, yarn.lock or pnpm-lock.yaml. Canaries, anything
below 11.1.4 and anything unparseable are reported unknown rather than
guessed, for the same reason as every other tool in this family: a confident
wrong answer is worse than an honest absent one.
Where it does not apply¶
Vercel's postmortem, published this morning, sets out the exposure by
deployment model
(Vercel).
Self-hosted applications using next start or output: 'standalone' were
affected. Deployments on Vercel, Netlify and Cloudflare Workers were not,
because routing runs in a separate system from the application there. Static
exports were never affected, since middleware needs a server runtime.
So the population this role is for is the self-hosted estate, which is the
population least likely to have been told. A managed platform fixed this for its
customers without their involvement; everyone running their own next start
behind their own proxy had to find out and act. That asymmetry matters, because
it is the recurring cost of self-hosting and it is paid in attention rather than
money.
What the scan cannot tell you¶
The advisory is about authorisation checks made in middleware. Our scan classifies by version, and does not examine whether an application uses middleware or what it does there. A vulnerable version whose middleware does nothing security-relevant is reported the same as one gating an admin route, and only a person reading the code can tell those apart.
The nginx parser is minimal on purpose and that cuts both ways. It does not
follow include directives, so a location block in an included file that
sets its own proxy_set_header will not get the snippet, and will not be
mentioned. It expects each block's { on the same line as server or
location, and reports anything else for handling by hand. List only
http-context files: a server block in a stream context does not accept
proxy_set_header, nginx -t fails and the role rolls back.
The snippet is also a stopgap that outlives its usefulness quietly. Upgrade
next, then take it out. A proxy stripping a header that no longer means
anything is harmless and misleading, and the next person to read that config
will spend twenty minutes working out which incident it came from.