Next.js CVE-2026-94545 解剖:一条查询参数打进 ROP 链的 9.5 分 RCE
2026 年 9 月底披露、10 月初被国内安全社区详细复现的 CVE-2026-94545(GHSA-vcvr-r3jv-pc5j),是近年来"组合拳漏洞"的教科书案例:Next.js 的 OG 社交分享图生成接口 ImageResponse 存在未认证远程代码执行,CVSS 4.0 高达 9.5 分(Critical)。
单看每一个组成漏洞,都是"中等严重度";拼在一起,就是一条可以从外部 HTTP 请求直达内存破坏的完整攻击链。
一、影响面:为什么几乎人人都中招
影响条件:
- next 16.2.0 – 16.3.5(16.2 线没有补丁版本,只能向前升级)
- 且 OG 图路由运行在 Node.js 运行时(Edge 运行时不受影响)
- 且外部可控值会传入图片生成内容
第三个条件就是关键——营销落地页、文档站、电商详情页的"分享卡片"功能近乎标配,而实现方式大多是:
// app/og/route.tsx —— 典型的可控输入实现
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') ?? '默认标题' // ❌ 外部输入直接进图片
return new ImageResponse(
(
<div style={{ display: 'flex', fontSize: 48 }}>
{title}
</div>
),
{ width: 1200, height: 630 }
)
}用户可控的 ?title= 参数最终会交给 satori(Vercel 的 JSX→SVG 渲染库)解析并绘制。整条链路无需认证、无需交互。
二、攻击链拆解:两个中危拼出一个满分
CVE-2026-94545 的精妙之处在于利用了两个不同层级的缺陷:
攻击链(文字版):
[HTTP 请求] GET /og?title=<精心构造的 payload>
│
▼
[层1: 转义缺失 — Next.js / 应用层]
特殊字符序列未被正确转义,穿越 JSX→satori 的
数据边界,注入原语成立(注入点 ≈ SVG/布局指令流)
│
▼
[层2: 解析器内存缺陷 — satori 原生/内存层]
恶意构造的解析输入触发越界写 / 类型混淆,
覆盖关键对象 → 控制流劫持
│
▼
[ROP] 借助 Node.js 进程内的已知 gadget 组装 ROP 链
│
▼
[RCE] 以 Next.js 进程权限执行任意代码2.1 层 1:转义缺失
上游(Next.js 应用侧 / satori 输入侧)没有对进入渲染内容的特殊序列做转义。这本身只是一个"内容注入"级别的问题——攻击者可以让生成的图片显示奇怪的内容,或者让渲染失败。
2.2 层 2:解析器内存缺陷
真正把严重度抬到 9.5 的是下游:satori(0.0.27 – 0.33.4)在解析被污染输入时存在内存缺陷。文本注入原语 + 解析器内存破坏 = 攻击者可以精确控制内存布局,进而劫持执行流。
这个组合暴露了一个结构性问题:JSX 生态的开发者默认渲染管线是"安全的字符串处理",实际上 satori 内部有接近原生代码的解析路径,心智模型必须按"处理不可信输入的解析器"来对齐——也就是说,要按 libpng、ImageMagick 那套标准来审。
三、验证与审计:拿到注入原语
赤霄攻防实验室在本地实测复现了注入原语,并给出了可直接跑的审计脚本。防守方可以用类似思路自查:
#!/usr/bin/env bash
# audit_cve_2026_94545.sh — 检测项目是否存在受影响的依赖组合
set -euo pipefail
check_dir() {
local dir="$1"
echo "== 检查 $dir =="
# 1. next 版本范围检测 (16.2.0 – 16.3.5)
if [[ -f "$dir/node_modules/next/package.json" ]]; then
ver=$(/usr/bin/python3 -c \
"import json;print(json.load(open('$dir/node_modules/next/package.json'))['version'])")
echo " next: $ver"
affected=$(/usr/bin/python3 - "$ver" <<'EOF'
import sys
v = sys.argv[1].split('.')
nums = tuple(int(x) for x in v[:3])
if (16,2,0) <= nums <= (16,3,5):
print("YES")
else:
print("NO")
EOF
)
[[ "$affected" == "YES" ]] && echo " [!!] next 版本在受影响范围 (16.2.0-16.3.5)"
fi
# 2. satori 版本范围检测 (0.0.27 – 0.33.4)
if [[ -f "$dir/node_modules/satori/package.json" ]]; then
ver=$(/usr/bin/python3 -c \
"import json;print(json.load(open('$dir/node_modules/satori/package.json'))['version'])")
echo " satori: $ver"
/usr/bin/python3 - "$ver" <<'EOF'
import sys
v = sys.argv[1].split('.')
nums = tuple(int(x) for x in v[:3])
if (0,0,27) <= nums <= (0,33,4):
print(" [!!] satori 版本在受影响范围 (0.0.27-0.33.4)")
EOF
fi
# 3. 检查 OG 路由是否运行在 Node.js 运行时且接收外部输入
if grep -rln "ImageResponse" "$dir/app" 2>/dev/null | while read -r f; do
if grep -q "searchParams\|params" "$f" && \
! grep -q "runtime = 'edge'\|runtime = \"edge\"" "$f"; then
echo " [!] 疑似暴露的 OG 路由(Node 运行时 + 外部输入): $f"
fi
done; then :; fi
}
for d in "$@"; do
check_dir "$d"
done在线侧缓解检测(升级完成前的临时手段):对 OG 路由的查询参数做严格白名单校验,拒绝包含特殊控制序列的请求;同时监控 OG 路由的异常 500 响应——渲染崩溃往往是攻击探测的前兆。
四、修复方案
4.1 立即升级(唯一彻底方案)
npm install next@16.3.6
# satori 若为直接依赖,一并升级
npm install satori@0.33.5- 修复版本:next 16.3.6(2026-09-22 发布)/ satori 0.33.5
- 15.5.26 为加固版(部分缓解)
- 16.2 线没有补丁版本,必须向前跨大版本升级
4.2 无法立即升级时的缓解措施
按优先级排序:
- 切断输入:OG 路由不再接收任何外部输入,分享图内容改为服务端静态生成或从数据库白名单取值;
- 迁移到 Edge 运行时:
export const runtime = 'edge',漏洞链的内存破坏部分不触发(注意 Edge 运行时的功能限制); - WAF 拦截:对 OG 路由的查询参数做长度限制 + 字符白名单,封禁疑似注入 payload 的请求模式。
// 缓解示例:白名单化 OG 路由输入
const ALLOWED_TITLES = new Set(loadTitlesFromCMS()) // 只允许预注册内容
export async function GET(request: Request) {
const title = new URL(request.url).searchParams.get('title') ?? ''
if (!ALLOWED_TITLES.has(title)) {
return new Response('Not Found', { status: 404 })
}
// ...渲染
}五、给工程团队的三个教训
"图片生成"是解析器,不是模板函数。 任何把外部输入送进渲染/解析管线的功能(OG 图、验证码图、PDF 导出、Markdown→图片),都应按"处理不可信二进制输入"的标准做威胁建模。ImageMagick 时代的教训(ImageTragick)在 JS 生态重演了一遍。
警惕中危漏洞的组合。 单独评估时"转义缺失"是低危、"解析器内存缺陷"是中危,但攻击链的严重度取决于最短路径而非单个环节的分数。安全审计要覆盖"数据从哪来、到哪去"的全链路,而不是孤立地给每个组件打分。
供应链组件的内存安全值得追问。 satori 这类底层渲染库的未来方向值得关注:Rust 解码器正在成为行业标配(Chrome 155 的 JPEG XL 解码就是用 Rust 重写的)。选择底层库时,"用什么语言写的、有没有 fuzzing CI"应该是选型维度之一。
六、结语
CVE-2026-94545 是典型的"新功能形态 + 旧类型缺陷":OG 分享图是 2020 年代的产品功能,而击穿它的却是 2000 年代就反复出现的转义缺失与内存破坏。升级到 next 16.3.6 是唯一彻底的解法,如果你的站点还在 16.2 线——那条线没有补丁,别等了。
参考资料
- GHSA-vcvr-r3jv-pc5j / GHSA-wx4j-mvgx-mqwp(Next.js / satori 官方通告)
- Next.js 16.3.6 Release Notes(2026-09-22)
- 赤霄攻防实验室:《一条查询参数打进 ROP 链:Next.js 分享图接口 9.5 分 RCE 深度分析》(2026-10-02)
- Vercel 开源漏洞赏金计划(发现者:KarimPwnz)
- ImageTragick — Command Injection in ImageMagick(历史同类案例)

