CVE-2025-29927
Overview of CVE-2025-29927
CVE-2025-29927 is a vulnerability in the middleware component of the popular Javascript framework Next.js; affecting a broad spectrum of versions from Next.js v11.x all the way up to v15.x. What is exceptionally fascinating about this vulnerability in particular is the ease of execution, the length of time it was left undetected, and the devestating implications of its potential exploitation with the capability to entirely bypass authentication mechanisms to view content that would otherise be restricted.
Affected versions:
Versions:
- > 11.1.4 < 12.3.5
- >= 13.0.0 < 13.5.9
- > 14.0 < 14.2.25
- > 15.0 < 15.2.3
Navigation:
- Next.js Middleware Logic Flaw
- middlewareInfo.name Values in Early Versions
- middlewareInfo.name Values in Later Versions
- Modern Next.js Versions and sandbox.ts Logic Flaws
- Detecting CVE-2025-29927
- Remediation
Next.js Middleware
Next.js is a complete framework which is shipped with its own middleware. Middleware allows for code to be run before a request is completed, allowing for a customized modification of the response depending on information from the request such as headers and cookies. These capabilities are widely used in production environments to handle path rewriting, sever-side redirects, authentication and authorization, and more.
For this vulnerability, the middleware usage that we are most interested in is authorization and protecting directories from being accessed based on conditional logic. Typically, middleware can ensure user identity and check session cookies before granting access to specific sites or API routes. For example, if an unauthenticated user attempts to access a protected page, /protected, their request will first be processed by the middleware, which checks if the request’s session cookies are valid, and determine whether to forward the request to the page or redirect the request to a login screen.
However, in an earlier version of the framework (v12.0.7), researchers noticed that the next-server.ts source code contained flawed logic handling requests with the header x-middleware-subrequest. When an application is using middleware, the runMiddleware() function is executed, of which the followig code is a part of.
Starting at line 686 in the runMiddleware() function in next/server/next-server.ts
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const allHeaders = new Headers()
let result: FetchEventResult | null = null
for (const middleware of this.middleware || []) {
if (middleware.match(params.parsedUrl.pathname)) {
if (!(await this.hasMiddleware(middleware.page, middleware.ssr))) {
console.warn(`The Edge Function for ${middleware.page} was not found`)
continue
}
await this.ensureMiddleware(middleware.page, middleware.ssr)
const middlewareInfo = getMiddlewareInfo({
dev: this.renderOpts.dev,
distDir: this.distDir,
page: middleware.page,
serverless: this._isLikeServerless,
})
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue
}
The following lines of code will retrieve the value of the x-middleware-request header:
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
However this logical if will check to see if the header contains the value for middlewareInfo.name, and if it does, then it decides to not apply the middleware and forward the request.
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
This effectively means that if a request is made with the x-middleware-subrequest header set with the correct value, then our requests will indescriminately bypass the middleware and be forwarded to the requested resource. This makes the x-middleware-subrequest act like a universal key, overruling any logic embedded in the middleware from having any impact or influence on requests with the only stipulation being an attacker’s ability to know the middlewareInfo.name value.
Guessing middlewareInfo.name Values
Unfortunately for Next.js, the value of middlewareInfo.name is easily guessable as it is the path of where the middlename is located in the application. In early versions of middleware’s implementation in Next.js (v12.2.x), the file that handled middleware had to be named _middleware.ts, and the only router that existed at the time was pages, leaving the only location for the file being pages/_middleware.ts.
If there are subdirectories with nested routes, any middleware logic will run from the top down. For example, an application with an admin page at /panels/admin with the following file structure:
- /pages
index.tsx
- /panels
_middleware.ts # Runs first
panels.tsx
- /admin
_middleware.ts # Runs second
admin.tsx
To exploit CVE-2025-29927 to gain access to /panels/admin, there are multiple possibilities for the middlewareInfo.name parameter and the following are potential payloads for x-middleware-subrequest:
x-middleware-subrequest: pages/_middleware
or
x-middleware-subrequest: pages/panels/_middleware
or
x-middleware-subrequest: pages/panels/admin/_middleware
Versions 12.2.x Onward middlewareInfo.name Values
Beginning with version Next.js 12.2 onward, the middleware logic file no longer contains underscores and must simply be named middleware.ts. It’s also no longer located in the /pages directory and is kept in the root directory. The payload for the versions starting with v12.2.x is simply:
x-middleware-subrequest: middleware
In the cases where a /src directory is created for the application, however, the payload would be:
x-middleware-subrequest: src/middleware
Going back to our earlier example, testing against an application running Next.js v14.0.0 and trying to navigate to the /protected directory which is protected by middleware:
After adding our payload header we can see our request has bypassed the middleware logic and granted us access to the /protected directory:
Modern Next.js Versions and sandbox.ts Logic Flaws
In Next.js, the middleware execution flow will set up a controlled runtime environment before executing the actual middleware logic. This runtime environment setup process is handled by the sandbox.ts file located at packages/next/src/server/web/middleware.ts. The following code was added in an attempt to prevent an infinite loop while handling recursive requests:
packages/next/src/server/web/sandbox/sandbox.ts starting from line 94:
export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
const runtime = await getRuntimeContext(params)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const MAX_RECURSION_DEPTH = 5
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)
if (depth >= MAX_RECURSION_DEPTH) {
return {
waitUntil: Promise.resolve(),
response: new runtime.context.Response(null, {
headers: {
'x-middleware-next': '1',
},
}),
}
}
The code iterates through the subrequests parameters and increments the depth variable for each value that is equal to params.name (which is just the path to the middleware). If the depth is greater than or equal to the MAX_RECURSION_DEPTH value, which by default is 5 then the middleware is again bypassed.
This leads to the following two payload possiblities:
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
or
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Detecting CVE-2025-29927
As simple as exploitation is for CVE-2025-29927, detecting its attempted exploitation is just as basic. Checking the web server logs manually can disclose evidence of exploitation, however the logs will need to record headers associated with HTTP requests. Any request containing the header x-middleware-subrequest is an indicator.
Remediation
Vercel has released patches for each version of Next.js resolving the vulnerability:
- Next.js 15.x -> Resolved in
15.2.3 - Next.js 14.x -> Resolved in
14.2.25 - Next.js 13.x -> Resolved in
13.5.9 - Next.js 12.x -> Resolved in
12.3.5 - Next.js 11.x -> Recommended workaround to drop external requests which contains the
x-middleware-subrequestheader.
Sources: