Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

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 运行时不受影响)
  • 且外部可控值会传入图片生成内容

第三个条件就是关键——营销落地页、文档站、电商详情页的"分享卡片"功能近乎标配,而实现方式大多是:

tsx
// 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 那套标准来审。

三、验证与审计:拿到注入原语 ​

赤霄攻防实验室在本地实测复现了注入原语,并给出了可直接跑的审计脚本。防守方可以用类似思路自查:

bash
#!/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 立即升级(唯一彻底方案) ​

bash
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 无法立即升级时的缓解措施 ​

按优先级排序:

  1. 切断输入:OG 路由不再接收任何外部输入,分享图内容改为服务端静态生成或从数据库白名单取值;
  2. 迁移到 Edge 运行时:export const runtime = 'edge',漏洞链的内存破坏部分不触发(注意 Edge 运行时的功能限制);
  3. WAF 拦截:对 OG 路由的查询参数做长度限制 + 字符白名单,封禁疑似注入 payload 的请求模式。
tsx
// 缓解示例:白名单化 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 })
  }
  // ...渲染
}

五、给工程团队的三个教训 ​

  1. "图片生成"是解析器,不是模板函数。 任何把外部输入送进渲染/解析管线的功能(OG 图、验证码图、PDF 导出、Markdown→图片),都应按"处理不可信二进制输入"的标准做威胁建模。ImageMagick 时代的教训(ImageTragick)在 JS 生态重演了一遍。

  2. 警惕中危漏洞的组合。 单独评估时"转义缺失"是低危、"解析器内存缺陷"是中危,但攻击链的严重度取决于最短路径而非单个环节的分数。安全审计要覆盖"数据从哪来、到哪去"的全链路,而不是孤立地给每个组件打分。

  3. 供应链组件的内存安全值得追问。 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(历史同类案例)

上次更新于: