TL;DR: A critical auth bypass vulnerability (CVE-2025-29927) in Next.js lets attackers skip middleware checks by faking the x-middleware-subrequest
header. It affects versions 11.1.4 through early 15.x. Managed hosts like Vercel were safe, but self-hosted apps relying on middleware for access control are at risk. Upgrade to a patched version (13.5.6, 14.2.24, 15.2.2+), or add auth checks directly in your protected routes if you can’t upgrade yet.
Next.js was recently at the center of a major web development controversy due to a critical vulnerability that allows unauthenticated users to bypass its authorization mechanisms. This vulnerability has been assigned the reference CVE-2025-29927 and a CVSS 3.1 score of 9.1.
These numbers may seem cryptic if you’re not familiar with security, but here’s what you need to know:
The vulnerability affects all versions of Next.js from 11.1.4 up to (but not including) the patched releases: 13.5.6, 14.2.24, and 15.2.2.
In this article, we’ll break down what causes this issue, how to prevent it, and how to structure your app to avoid being exposed.
Discovered by two security researchers who go by the pseudonyms zhero and inzo while digging into the Next.js source code, this vulnerability lets attackers bypass security checks by manipulating an internal HTTP header called x-middleware-subrequest
. If you’ve worked on a Next.js application with auth, you know the middleware is usually where you’d handle these checks to make sure users are allowed to access certain pages and redirect them based on their role.
Say you’re building a SaaS app. You’d typically check in the middleware if a user is a paying customer before letting them access paid features, or redirect them to the pricing page if they’re not:
// middleware.js import { NextResponse } from 'next/server'; // Helper functions async function isValidSession(sessionToken) { … } async function isPaidUser(sessionToken) { … } export async function middleware(request) { const { pathname } = request.nextUrl; // Paths to dashboard and pricing page const dashboardPath = '/dashboard'; const pricingPath = '/pricing'; // Check if the user is trying to access the dashboard or any paid features area if (path.startsWith(dashboardPath) || path.startsWith('/paid-features')) { // Get the session cookie const session = request.cookies.get('session')?.value; // If session is not valid, redirect to login if (!session || !(await isValidSession(session))) { return NextResponse.redirect(new URL('/login', request.url)); } // Check if the user is a paid user if (!(await isPaidUser(session))) { // If not a paid user, redirect to the pricing page return NextResponse.redirect(new URL(pricingPath, request.url)); } // If the user has a valid session and is a paid user, allow access return NextResponse.next(); } // For other paths, continue with the request return NextResponse.next(); }
This middleware vulnerability gives attackers a way to bypass these checks and access protected routes in your app, basically letting them use paid features without paying.
Middleware is a core concept in many web frameworks. It’s a layer of code that sits between the request and response cycle of an application, often used to perform generic actions like error handling, authentication, authorization, path rewrites, and more.
Next.js follows this same idea, but with a twist: its middleware runs by default on every route. That’s different from other frameworks, where middleware usually needs to be attached to specific routes or route groups. Next.js does let you limit its scope using route matching, but the default behavior is global.
Because of this, the middleware is a common place to handle security logic like authentication and authorization, so you don’t have to repeat it across routes.
That global behavior, combined with the internal x-middleware-subrequest
header, created an exploitable vulnerability that attackers used to bypass security measures entirely.
The vulnerability centers around the x-middleware-subrequest header
, which is meant to prevent infinite middleware recursion. Below is a screenshot from the blog post published by the researchers who found the bug, showing how this header is used in the Next.js codebase:
This part of the codebase runs your middleware and uses the x-middleware-subrequest
header to track how many times it has been executed during a single request.
When a user visits a protected route, the middleware might use NextResponse.rewrite()
or NextResponse.redirect()
to either rewrite the request to a different internal path or send a redirect back to the client after successful authentication. For example, if a user logs in, the middleware might rewrite /login
to /admin
. Since this triggers a new internal request to /admin
, Next.js considers it a subrequest.
Let’s say the user is authorized and tries to load a resource, like an image, that requires an internal fetch to the server. If that fetch also triggers middleware logic (e.g., for checking session data), it becomes another subrequest.
To prevent the middleware from getting stuck in an infinite loop, Next.js tracks the chain of these subrequests using the x-middleware-subrequest
header. It appends the middleware name to the header as a colon-separated string. Each time the middleware runs, it reads this header, splits it into an array, and counts how many times its own name appears.
If the middleware has been triggered more than five times (as defined by the MAX_RECURSION_DEPTH
constant), Next.js halts any further middleware execution for that request.
By manually adding the x-middleware-subrequest
header with the right value, an attacker can trick Next.js into thinking the middleware has already run five times, when it hasn’t run at all.
This will cause the runtime to skip the middleware completely. Execution will jump directly to the if
statement, exit early, and forward the request via NextResponse.next()
, sending the request through without any authentication or authorization checks.
Then, all an attacker would need is the name and path of the middleware file. Which, due to Next.js’ predictable naming conventions, is relatively easy to guess.
In older versions of Next.js (v11 to v12), middleware files had to be named _middleware.ts
and placed inside the pages
directory. That made them easy to locate, so the exploit could be used like this:
x-middleware-subrequest: pages/_middleware
In newer versions (v12.2 and up, including v13+), the naming convention changed to middleware.ts
, and the file is typically placed in the project root, or the src
directory if that’s configured. This makes the exploit just as easy to apply, and it would look something like this:
x-middleware-subrequest: middleware
Or:
x-middleware-subrequest: src/middleware
In newer versions, starting with v14+, the logic changed slightly. To make the exploit work, the header value now needs to be repeated multiple times:
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
Or:
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Contrary to what you might think, not every Next.js app was affected by this vulnerability. Apps hosted on managed platforms like Vercel and Netlify were automatically protected. The same goes for static sites because they don’t rely on middleware, and apps using Cloudflare with properly configured (WAF) rules.
The real risk was for self-hosted apps that rely on middleware for security checks without any fallback or secondary validation. You might think there aren’t many self-hosted Next.js apps out there, but with all the recent noise about Vercel’s surprise billing, more devs are moving to self-hosted setups.
I’m not going to dive into the many advanced ways to fix this vulnerability; I’ll leave that to the experts. What I will do is show you, as a frontend developer, how you can protect your Next.js applications from this vulnerability.
As I mentioned earlier, this vulnerability has been patched in the latest versions of Next.js, so the fastest way to secure your app is to update to the latest release (v15 at the time of writing). But not everyone can do that right away. This vulnerability affects old versions, and plenty of teams stick with older versions of Next.js for the sake of stability or to avoid breaking changes. Updating might not be an easy lift.
If that’s the case, your best option is to add extra security checks directly inside protected routes. For example, on an admin page, you could do something like this:
import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { redirect } from "next/navigation"; export default async function DashboardPage() { const session = await getServerSession(authOptions); // Session check if (!session) { redirect("/api/auth/signin"); } // Session integrity check const isSessionValid = verifySessionIntegrity(session); if (!isSessionValid) { redirect("/api/auth/signout?error=session-integrity"); } return ( … ); } // Helper function async function verifySessionIntegrity(session) { // Check for session tampering signs if (!session.user?.email || !session.user?.id) return false; // Compare with database record for consistency const userRecord = await getUserFromDb(session.user.id); if (!userRecord) return false; return true; }
I know this almost defeats the whole point of using middleware in the first place. It can get tedious, especially if your app has many pages. But if you’re stuck on an older version, this extra step is a small price to pay to keep your app secure.
Always stay informed about security advisories for all your dependencies. Subscribe to security mailing lists and enable security alerts for your repositories to catch vulnerabilities early.
Update your dependencies regularly. Considering that this vulnerability traces back as far as v11, regular updates are crucial to maintaining security.
Be cautious when relying on HTTP headers for security-related decisions. They can be easily manipulated by bad actors.
At first glance, Next.js’s middleware vulnerability might not seem like a big deal. Maybe it just lets someone use a paid app for free, right? Not exactly. It’s much more serious for apps that expose sensitive data, like internal reports or confidential documents, that aren’t strictly tied to a user account. In those cases, unauthorized access could lead to compliance issues and serious damage to your reputation.
The goal of this article is to help developers and app owners understand the risk and take steps to protect their Next.js apps. It’s been a little over a month since the patch was released, but most of the conversation has stayed on social media. Hopefully, this gives some clarity to those who might have missed it.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket captures console logs, errors, network requests, and pixel-perfect DOM recordings from user sessions and lets you replay them as users saw it, eliminating guesswork around why bugs happen — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Derek Pharr, CPO at Sporcle, Inc., talks about the power of community and how it’s shaped Sporcle’s product and company.
Behind every smooth user journey is a content designer shaping the words, structure, and flow. Here’s how they do it.
Whether you’re already managing UX teams or eyeing the leap, this guide shows what the job really entails — and how to thrive without losing sight of the users.
Build a secure file upload system using Astro’s server-side rendering, Cloudinary’s SDKs, and native integration.