Next.jsCVE-2026-94545分析:next/og里没转义的引号,和一个没讲完的RCE"

admin 2026-10-16 05:55:07 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

Next.js CVE-2026-94545 分析:next/og 里没转义的引号,和一个没讲完的 RCE

原创

Kratos
Kratos

Kratos Sec

2026年9月30日 22:06
河南

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

Next.js CVE-2026-94545 分析:next/og 里没转义的引号,和一个没讲完的 RCE

9 月 22 日 Vercel 发了一次带外(out-of-band)安全更新,Next.js 16.3.6 修复 CVE-2026-94545:next/og 的 ImageResponse 在 Node.js 运行时下可被未授权用户利用,CVSS 4.0 给到 9.5。有意思的是,这个洞的根因不在 Next.js 自己的代码里,而在上游的 JSX 转 SVG 引擎 satori——一处从项目早期就存在的字符串裸拼接。这篇从代码层面把注入点、补丁、复现和利用边界讲清楚,重点回答两个被预警稿模糊掉的问题:SVG 注入到底怎么升级成 RCE,以及你的业务到底中不中招。

0x0 漏洞概述

项目 内容
CVE 编号 CVE-2026-94545 / GHSA-vcvr-r3jv-pc5j
上游 advisory GHSA-wx4j-mvgx-mqwp(satori)
漏洞类型 SVG 注入 → 远程代码执行
CVSS 评分 9.5(CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H)
影响组件 next/og 的 Node.js 版 ImageResponse(底层 satori)
影响版本 Next.js >=16.2.0 <16.3.6;satori >=0.0.27 <0.33.5
修复版本 Next.js 16.3.6(15.5.26 仅加固,15.x 不受 RCE 影响);satori 0.33.5
报告者 RaghavMaheshwari124、rafabd1(Vercel HackerOne)
在野利用 截至 9 月 23 日无公开利用报告

时间线:9 月 22 日 Next.js 与 satori 的 advisory 同步公开,16.3.6、15.5.26、satori 0.33.5 同日发布;9 月 23 日社区出现 Docker 复现实验室,注入原语端到端跑通。修复 commit 是 satori 仓库的 26a52af,标题叫\"Harden SVG serialization\",8 个文件,+262/-86。

0x1 next/og 是怎么画图的

很多业务用 ImageResponse 动态生成分享卡片:/og?title=xxx 返回一张带标题的 PNG,用于社交平台预览。这条渲染链在 Node.js 运行时下是这样的:

路由 handler(Node.js runtime)
   └─ ImageResponse(next/og,只是个壳)
        └─ @vercel/og index.node.js(预编译进 next 包)
             ├─ satori:JSX + CSS ──字符串拼接──▶ SVG 字符串
             └─ Resvg:SVG ──光栅化──▶ PNG

next/og 本体几乎不含逻辑,image-response.ts 里按 NEXT_RUNTIME 选择加载 index.node.js 还是 index.edge.js,真正的渲染在预编译的 @vercel/og 里。这就是为什么这次修复不体现在 Next.js 的某一行代码上,而是整体升级打包进去的 satori 及相关依赖。

satori 负责 HTML/CSS 布局(排版交给 yoga),最后把布局结果序列化成 SVG 字符串。注意这个\"序列化\":不是走 XML 库,而是手工模板字符串拼接。漏洞就长在这里。

0x2 代码层面:四个注入点

核心缺口在 buildXMLString。 这个函数是 satori 生成所有 SVG 元素的出口,修复前的实现:

// satori/src/utils.ts(修复前)
export function buildXMLString(type, attrs, children) {
  let attrString = ''
  for (const [k, _v] of Object.entries(attrs)) {
    if (typeof _v !== 'undefined') {
      attrString += ` ${k}=\"${_v}\"`
    }
  }
  ...
}

属性值 _v 原样塞进双引号,没有任何转义。只要值里带一个 \",就能闭合当前属性,后面的内容全部变成攻击者定义的新属性乃至新元素。

正常文本反而逃不出去,style 能。 文本内容走的是另一条路,builder/text.ts 里对 content 老老实实做了 escapeHTML:

// satori/src/builder/text.ts(修复前,节选)
buildXMLString(
  'text',
  {
    ...shapeProps,
    fill: style.color,                 // ← 不转义
    'font-family': style.fontFamily,   // ← 不转义
    'letter-spacing': style.letterSpacing,
    stroke: style.WebkitTextStrokeColor,
    style: style.filter ? `filter:${style.filter}` : undefined,
  },
  escapeHTML(content)                  // ← 文本内容有转义
)

所以 {userText} 这种写法注入不了,但 style 派生出来的属性(color→fill、fontFamily、letterSpacing、WebkitTextStrokeColor、filter 等)全部走裸拼接。color: 'red\"/><path data-pwned=\"true\"/>' 一类值可以 直接打穿——补丁新增的测试用例里就是这个 payload。

内联 <svg> 路径是重灾区。 satori 支持在 JSX 里直接写 <svg> 子元素,这条路径在 preprocess.ts 的 translateSVGNodeToSVGString,三处裸拼:

// satori/src/handler/preprocess.ts(修复前,节选)
if (typeof node !== 'object') return String(node)      // 文本子节点,原样返回
...
return ` ${ATTRIBUTE_MAPPING[k] || k}=\"${_v}\"`          // 属性名和值都原样
...
` style=\"${Object.entries(style).map(([k, _v]) => `${midline(k)}:${_v}`).join(';')}\"`

官方 advisory 给的受影响示例正是打在这里——query 参数直接进 <title>:

import { ImageResponse } from 'next/og'

export function GET(request) {
  const value = new URL(request.url).searchParams.get('value') ?? ''
  return new ImageResponse(
    <svg width=\"1200\" height=\"630\">
      <title>{value}</title>
    </svg>
  )
}

还有两个助攻缺口。 一是内联 SVG 转成 data URL 时用正则做百分号编码,字符集 [\\r\ %#()<>?[\\\\\\]^\"']漏了&,嵌套 SVG 里的 HTML 实体会穿过外层解析复活成标记;二是expand.ts对下划线前缀的\"内部样式属性\"(如\_inheritedClipPathId`)直接透传,调用方可伪造内部字段混进序列化。

补丁是收口式修复。26a52af 把散落在各 builder 里的手工拼接全部收敛到 buildXMLString,属性值经 escapeXMLAttribute(内部用 escape-html),文本经 escapeXMLText;名字没法转义,就照 React 的做法用 XML NameChar 正则白名单校验,非法直接抛错;_ 前缀属性从透传改成 throw;& 补进编码字符集。修的是一整类问题,不是被报告的那一个实例。

0x3 复现

社区的复现实验室(Hassham1/CVE-2026-94545-nextjs-og-poc)用官方 advisory 的路由模式起两个容器:16.3.5 和 16.3.6,payload 走 query:

GET /og?value=</title><rect width=\"500\" height=\"150\" fill=\"#FF0000\"/>

payload 要点:先闭合 <title>,再注入一个 500×150 的纯红矩形。注意整个 payload 必须是配平的 XML,标签不闭合会把内层文档的解析搞挂。

验证方式很讲究。返回的是 PNG,肉眼看\"图上多了个红块\"不够客观,实验室的扫描脚本把 PNG 的 IDAT 解开后逐像素统计,结果:

目标 结果
next 16.3.5 75,000 个 #FF0000 像素,注入成立
next 16.3.6 payload 被转义成文本,0 个红像素
satori 0.25.0 payload 原样出现在序列化 SVG 里

三个实践坑值得记下:光栅化器里没有可加载字体,注入 <text> 渲染出来不可见,验证标记要用 <rect> 这类纯形状;响应大小启发式不靠谱,修复版把 payload 当可见文本渲染出来,响应同样变大;想写 nuclei 模板的话,matcher 解不了 PNG,官方特征没法直接用。

0x4 冷静一下:RCE 到底证没证实

把公开信息排开看,这件事要分两层说。

注入原语是板上钉钉的。 未授权、单请求、攻击者完整控制 SVG 文档内容,16.3.5 上 75,000 个红像素就是证据。satori 的转义缺陷从 0.0.27 就在,PoC 仓库在 0.25.0 上也复现了裸注入。

RCE 完整链条没有公开。 Vercel 的原文是\"不当转义……由于其他上游依赖中的漏洞,可能导致远程代码执行\"——升级到 RCE 的那一环发生在消费 SVG 的下游组件(Node 实现里的 resvg 光栅化链路)里,但具体是哪个依赖、什么缺陷,advisory 没有点名,细节未公开。ikzero 和 Checkmarx 的分析文都明确说自己验证到注入为止,RCE 没走通也没证伪。修复方式侧面印证了这一点:16.3.6 是把 satori 和这些依赖一并升级,而不是改掉某处逻辑。

两个评分的落差也在这里。Next.js 给 9.5,靠的是 SC:H/SI:H/SA:H(对后续系统的高影响)加上 AT:P(有利用前提);satori 自己只评 5.3 Moderate,因为对库来说这就是个转义缺陷,后果取决于下游怎么消费 SVG。AT:P 换成人话:业务必须把用户输入拼进 SVG 的内容、属性或样式。分享卡片接口把标题、用户名、评论片段塞进 OG 图,是再常见不过的写法,这个前提在实际业务里并不苛刻。

Edge 不受影响是官方口径:next/og 在 Edge 运行时加载的是另一套 index.edge.js 构建,官方明确 Edge 实现不在 RCE 影响范围内。另外直接使用 satori 的项目(不止 Next.js)都该对号入座,影响要看你的 SVG 落到谁手里。

即便 RCE 一时不成立,注入本身已经意味着攻击者可以在服务器上驱动一个 SVG 解析器去解析任意构造的文档,社区也点名了外部资源引用带来 SSRF/文件读取的可能性,同样属于未验证但值得警惕的方向。

0x5 自查:判断自己有没有暴露

第一步看版本。npm ls next,落在 >=16.2.0 <16.3.6 就在 RCE 影响范围内。15.x 不受 RCE 影响,但建议跟上 15.5.26 的加固。项目直接依赖 satori 的,查是否 <0.33.5。

第二步比版本更重要:查数据流。 全局搜这几个关键词:

  • next/og、ImageResponse、@vercel/og
  • 文件约定路由 opengraph-image、twitter-image
  • 直接使用 satori( 的代码

命中之后看渲染入参有没有用户可控值流入 JSX 文本、内联 <svg> 的属性、style 对象。重点盯三类写法:searchParams.get(...) 进 JSX、请求体里的字段进 JSX、动态路由参数进 JSX。export const runtime = 'edge' 的路由不在 RCE 范围内,但注入风险同样存在。

第三步翻日志。 对 OG 类路由回溯请求参数,找这些特征:</title>、\"/><、<rect、<path、<image、<style 出现在 query 或请求体里。这类路由平时流量不会带 SVG 片段,特征很干净。

0x6 修复与应急

升级优先。npm install [email protected](16.3 线),15.x 升 15.5.26,直接用 satori 的升 0.33.5。补丁在预编译产物里,重启生效,没有迁移成本。

暂时不能升级的,用官方 workaround: 不把攻击者可控的值传进 SVG 的内容、属性和样式。落到操作上:OG 路由的动态参数走白名单;必须渲染用户文本的,进 JSX 前先做 HTML 转义;style 对象里的颜色、字体等字段不要拼接用户输入。

怀疑被打过的,升级之外做两件事。 主机侧排查异常进程、计划任务、对外连接;应用侧核对 OG 路由的历史访问日志,把带注入特征的请求 IP 和时间线拉出来。截至目前没有公开在野利用的报告,但 PoC 和检测脚本已经流通一周,自建 Next.js 服务在公网上的不该赌这个时间差。


参考

  • https://github.com/vercel/next.js/security/advisories/GHSA-vcvr-r3jv-pc5j
  • https://github.com/vercel/satori/security/advisories/GHSA-wx4j-mvgx-mqwp

免责声明:本文仅用于安全研究与学习,未经授权不得用于任何非法渗透测试活动。


对1000份网安简历的分析" 网络安全文章

对1000份网安简历的分析"

该文档基于约1000份网安简历的统计分析,指出简历同质化严重,渗透测试等关键词滥用,动词缺乏具体对象和动作。建议求职者补充项目细节、个人分工与量化成果,明确测试边界,以提升简历辨识度。
网络安全应急响应专家智能体更新:6大能力域" 网络安全文章

网络安全应急响应专家智能体更新:6大能力域"

网络安全应急响应专家智能体更新至v1.31.28,新增数据库取证、供应链投毒取证、钓鱼邮件与BEC、内存马与混淆Webshell、容器逃逸复现、Windows域控攻击链六大能力域,实测基线增至79项,并自我纠正撤回2条不成立判据。文档强调真机实测结论,提供可操作处置建议,并明确能力边界。
评论:0   参与:  0