One header skipped my Next.js middleware, and my auth with it
CVE-2025-29927 let one request header skip Next.js middleware. I tested my self-hosted apps, patched them and moved auth next to the data.

A Next.js advisory with a CVSS of 9.1 came out on Friday, and by Saturday morning my timeline was full of the same curl command. I spent a good part of the weekend on it, partly patching and partly feeling a bit daft about where I'd put auth in a couple of apps.
What the bug was
Middleware runs before a request reaches your page or route handler, which is why so many of us put "no session cookie, redirect to /login" there. One file, every route covered.
Internally, Next.js has to stop middleware from calling itself in a loop, so it tags its own internal requests with a header, x-middleware-subrequest, and skips middleware when it sees it. The trouble was that nothing checked where that header came from. Anyone could send it, the server would treat the request as one of its own, and middleware simply didn't run. If middleware was your only auth check, the check was gone (along with anything else you did there, like CSP headers or a maintenance-page rewrite).
It's CVE-2025-29927, advisory GHSA-f82v-jwr5-mffw, disclosed on 21 March 2025. Self-hosted apps running next start or the standalone server were exposed, which is exactly how I run things in Docker at home. Vercel and Netlify weren't, and static exports have no middleware to skip. The fixes are 15.2.3, 14.2.25, 13.5.9 and 12.3.5.
Testing my own apps
The value the header needs depends on the version. Older releases want the middleware's name (middleware, or src/middleware if you use a src/ folder). Newer ones count recursion depth and give up after five, so the value is the name repeated five times with colons. I only tried this against apps I own, from my own machine:
curl -s -o /dev/null -w '%{http_code}\n' https://myapp.example.com/dashboard
# 307, off to /login as expected
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' \
https://myapp.example.com/dashboard
# 200
That 200 was not a fun thing to see. An internal tool on 15.1 rendered its dashboard with no session at all. Its data didn't load, because the API behind it checked the token itself, but the page shell came back with nav items and labels that had no business being public. On an older 14.x side project the src/middleware form was the one that worked, so try both if you're checking.
Patching was dull, which is how you want it:
npm install next@15.2.3
npm ls next # make sure nothing else is pulling in an older copy
docker build --no-cache -t myapp:latest .
I built without cache because I didn't want to find out on Monday that a layer was still holding the old node_modules. After the redeploy, the same curl gave a 307. My apps sit behind a reverse proxy, so I also strip the header there; there's no reason a request from outside should ever carry an internal framework header, patch or no patch:
location / {
proxy_set_header x-middleware-subrequest "";
proxy_pass http://127.0.0.1:3000;
}
An empty value in nginx means the header isn't forwarded at all. On Cloudflare, a transform rule that removes the request header does the same thing at the edge.
Where the auth should have been
The patch was the easy bit. The uncomfortable bit was the shape of the internal tool. Middleware read the cookie, verified the session and redirected, and the page simply assumed that if it was rendering, the user must be allowed:
// middleware.ts
export async function middleware(req: NextRequest) {
const session = await getSession(req);
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };
// app/dashboard/page.tsx
export default async function Dashboard() {
const reports = await db.report.findMany(); // nobody asks who is asking
return <ReportList reports={reports} />;
}
Plenty of tutorials teach exactly this, and it means one framework bug becomes an auth bypass. Now the page asks who the user is, and the query is scoped to them:
// lib/auth.ts
import "server-only";
export async function requireUser() {
const session = await getSession();
if (!session) redirect("/login");
return session.user;
}
// app/dashboard/page.tsx
export default async function Dashboard() {
const user = await requireUser();
const reports = await db.report.findMany({ where: { orgId: user.orgId } });
return <ReportList reports={reports} />;
}
Middleware still does the redirect, since it's nicer for people to bounce to login before anything renders, but it's a convenience now. The lock sits next to the data.
The API that saved me is written in Go, and every handler checks the token because there's no magic file in Go that runs first. Slightly funny that the more boring stack had the safer habit.
If you're still on an old version, upgrade to the patched release for your line. If you really can't this week, strip x-middleware-subrequest at your proxy or CDN today, then go through anything that relies on middleware for auth and add a check in the page, route handler or server action itself.
First published on my blog. Cover photo: ImagePerson, CC BY-SA 4.0, via Wikimedia Commons (source).
About the author
- Full stack engineer in London with 5+ years on production web platforms. I lead AI-assisted modernisation at Xanda: React and Go for the UK energy market, a government claims portal, and moving old Java and C++ systems onto the cloud. Outside work I run a small home server behind a Cloudflare Tunnel with a handful of public tools on it: Doorstep (rental alerts across Rightmove, OnTheMarket and OpenRent), SeenSooner (NHS A&E and planned-care waits), GridPulse (live GB grid carbon), Sift (LLM ticket triage that survives model failures) and AI Radar (a news digest written hourly by an n8n workflow). I write here about React and Next.js upgrades, Node, coding with AI agents, and the CVEs I've had to patch. Originals live at blogs.kishankanejiiya.tech.
Comments
Join the discussionNo comments yet. Be the first to comment.




