AxisOps Harbour
Labs

Insights

Mitigation code available CVE-2025-29927

A header you trusted because it was yours

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.