DEV Community

Emil Alander
Emil Alander

Posted on • Originally published at guardlayer.io

Next.js Middleware Auth Bypass: CVE-2025-29927

A spoofable x-middleware-subrequest header let requests skip Next.js middleware, bypassing auth. Upgrade to the patched version; don't trust middleware alone.

TL;DR — CVE-2025-29927 is a critical (CVSS 9.1) authorization bypass in Next.js. The framework used an internal header, x-middleware-subrequest, to stop middleware from recursing — and it trusted that header even on requests coming from the outside. So an attacker could add x-middleware-subrequest to any request and Next.js would skip middleware entirely, sailing straight past any auth or redirect logic that lived there. The fix: upgrade Next.js to the patched release for your major (15.2.3, 14.2.25, 13.5.9, or 12.3.5). The deeper fix: never make middleware the sole authorization gate — re-check auth in the route handler, server action, and data layer too.

Is my Next.js app vulnerable to CVE-2025-29927?

If you run a self-hosted Next.js app (using next start or output: 'standalone') on an affected version, and you gate access to protected routes in middleware.ts, then yes — you were exposed.

The affected and patched ranges, straight from the GitHub advisory (GHSA-f82v-jwr5-mffw) and NVD:

Release line Affected range Fixed in
15.x ≥ 15.0.0, < 15.2.3 15.2.3
14.x ≥ 14.0.0, < 14.2.25 14.2.25
13.x ≥ 13.0.0, < 13.5.9 13.5.9
12.x / 11.x ≥ 11.1.4, < 12.3.5 12.3.5

The bug was introduced at 11.1.4. There is no separate 11.x patch — if you're on 11.x, you must move to 12.3.5 (or apply the header workaround and move auth out of middleware).

Check the resolved version, not the range in package.json — a caret range like "next": "^14.1.0" tells you what's allowed, not what's installed:

npx next --version   # the actually-resolved binary
npm ls next          # the lockfile-resolved version in the tree
npm audit            # flags GHSA-f82v-jwr5-mffw until you're patched
Enter fullscreen mode Exit fullscreen mode

Who was NOT exposed: apps on Vercel and Netlify. Vercel's routing runs decoupled from the Next.js server, so the client-supplied header never reached the middleware evaluation the way it did in self-hosted setups. Static exports and Cloudflare Workers deployments were likewise not impacted.

How does the middleware bypass work?

Next.js middleware runs before a request reaches a route — perfect for auth gating, redirects, and header rewriting. Internally, Next.js can re-invoke middleware as part of its own request handling, so it needs a way to stop that from recursing forever. It used an internal request header, x-middleware-subrequest, to flag a request as an internal subrequest and tell the framework to skip running middleware.

The flaw: that header was trusted unconditionally. Nothing verified it originated internally rather than from the client. Add the header to an inbound request and Next.js skips middleware — while still serving the underlying route. Any authorization decision in middleware was silently bypassed.

The expected value tracked how Next.js evolved its recursion guard (per source-level analyses from Datadog Security Labs and ProjectDiscovery):

  • Pre-12.2: the middleware file path, e.g. x-middleware-subrequest: pages/_middleware.
  • ~12.2–13.1.x: a single value like middleware (or src/middleware for a src/ layout).
  • 13.2.0+ / 14.x / 15.x: a recursion-depth guard that counts occurrences and skips once the count exceeds MAX_RECURSION_DEPTH (default 5), so the payload became the identifier repeated five times, colon-separated: middleware:middleware:middleware:middleware:middleware.

Remediation: upgrade first

The real remedy is upgrading Next.js to the patched release for your major line. Pin to at least the patched version so you get the fix without an unwanted major jump:

npm install next@14.2.25    # or next@^14.2.25 to take later patches on your major
Enter fullscreen mode Exit fullscreen mode

A static dependency scanner flags exactly this — an outdated, vulnerable Next.js version sitting in package.json:

{
  "dependencies": {
    "next": "14.1.0",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

(See GuardLayer's live scan flag that version as vulnerable on the original post.) Be precise about what that catches: the tool flags the outdated version — it does not detect the x-middleware-subrequest header spoof at the code level; nothing static in your handlers reveals that a header was trusted at the framework layer. The version flag is the actionable signal, and upgrading is the fix. For the broader picture, see auditing vulnerable npm dependencies.

If you can't upgrade immediately: strip the header at the edge

The advisory's documented workaround is to prevent external requests carrying x-middleware-subrequest from reaching your app. Because the header is what causes middleware to be skipped, the stripping must happen at a layer in front of the app.

nginx reverse proxy — clear any client-supplied value before forwarding upstream:

location / {
    proxy_set_header x-middleware-subrequest "";
    proxy_pass http://nextjs_upstream;
}
Enter fullscreen mode Exit fullscreen mode

Cloudflare / generic WAF — block the request outright:

(any(http.request.headers.names[*] == "x-middleware-subrequest"))
# Action: Block
Enter fullscreen mode Exit fullscreen mode

Caveats: this only protects paths that actually pass through that proxy — if your origin is reachable directly, the header sails through, so strip it at every ingress. HTTP header names are case-insensitive, so match regardless of casing. And it's a stopgap, not a repair of the underlying trust bug. (Cloudflare shipped a managed WAF rule on March 22, 2025 that blocks this header — but it's opt-in, not on by default, because auto-blocking broke sites whose third-party auth middleware legitimately relied on the header.)

The durable lesson: middleware is not your only authorization layer

Vercel's postmortem says it directly: "We do not recommend Middleware to be the sole method of protecting routes." Middleware is best for cheap, coarse redirects — bouncing an unauthenticated visitor toward a login page for nicer UX — not the last line of defense. Re-check authorization at the layer that actually returns the data:

// app/api/admin/users/route.ts
import { NextResponse } from "next/server";
import { getSession } from "@/lib/auth";

export async function GET() {
  const session = await getSession();               // re-check HERE, not just in middleware
  if (!session) return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
  if (session.user.role !== "admin") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  return NextResponse.json({ users: await db.user.findMany() });
}
Enter fullscreen mode Exit fullscreen mode

Do the same in server components (redirect("/login") at render time) and in server actions, where auth must be verified inside the action itself. The strongest backstop is pushing authorization into a data-access layer and enabling database Row-Level Security, so a request that skips every application-layer check still can't read rows it doesn't own. That layered posture — covered in the Next.js app-layer security guide — is what turns a single-layer bug like CVE-2025-29927 into a non-event.

Confirm you're patched

Send the exploit header at a protected route and compare it to the same request without the header. On a patched build they must be identical — both denied:

curl -i https://your-app.example.com/admin -H "x-middleware-subrequest: middleware"
# Patched: same 302/401/403 as a request WITHOUT the header
# Vulnerable: returns the protected page (bypass succeeded)
Enter fullscreen mode Exit fullscreen mode

If adding the header changes the outcome, you're still vulnerable.


I write these while building GuardLayer, a static scanner for Next.js + Supabase apps. Originally published on the GuardLayer blog.

Top comments (0)