next/og ImageResponse SVGインジェクション (GHSA-vcvr-r3jv-pc5j)CVE-2026-94545(Next.js ImageResponse のSVGシリアライゼーションインジェクション、上流: Satori GHSA-wx4j-mvgx-mqwp)を、分離されたループバック専用ラボで再現・検証するためのセキュリティリサーチ資料です。
>=16.2.0 <16.3.6(Node.js ImageResponse のみ)26a52aff)。15.5.26 には関連するハードニングが含まれるが、15.x はこのRCE問題の影響を受けない自身が所有するか、明示的にテストを許可されたシステムでのみ使用してください。 スキャナは非破壊的です。画像をレンダリングし、ピクセルをカウントするだけです。ターゲット上に何かを書き込んだり、保存したり、実行したりすることはありません。
アドバイザリに記載された正確なルートパターン(攻撃者の値が <svg><title>{value}</title></svg> に入る)に対して:
next 16.3.5 (脆弱): 注入された 500x150 の赤い rect が PNG にレンダリングされた
(レスポンス内で 75,000 個の #FF0000 ピクセルを計測)
next 16.3.6 (修正済): 値がエスケープされ、赤ピクセルは 0
satori 0.25.0 (脆弱): シリアライズされた SVG 文字列内にペイロードが生のまま存在
satori 0.33.5 (修正済): ペイロードが </title> にエスケープされた
これはインジェクションプリミティブをエンドツーエンドで証明しています(シリアライザ -> 埋め込みSVG -> ラスタライズ出力)。ただし、リモートコード実行は実証していません。アドバイザリはRCEを下流のSVGパーサーに結び付けていますが、その脆弱な詳細は執筆時点で公開されていません。ファイル読み取り、SSRF、コード実行は主張していません。
Satoriは <svg> ノードを translateSVGNodeToSVGString()(src/handler/preprocess.ts)でシリアライズしており、これがエスケープせずに出力XMLに補間していました:
String(node) 経由で返され、エスケープされていなかった(現在は escapeXMLText)。" で脱出可能。style の値は生のまま補間されていた。& をエンコードしていなかったため、内部XMLエンティティが外側のSVGパースを生き延びていた。expand.ts が内部スタイルプロパティ(プレフィックス _)をシリアライズされたスタイルに渡していた - 現在は拒否される。修正(satori@26a52aff)は、すべてをハードニングされた buildXMLString() 経由にし、値をエスケープしてXML名を検証します。このラボはそのすべてを動作的に検証します。正確なリクエストとピクセルの証拠は poc/ にあります。
要件: Docker(Compose v2)、ホスト上の Python 3.10+(スキャナ用)、初回実行時のnpmへのネットワークアクセス。
./lab verify
脆弱なアプリ([email protected] からベンダリングされた next/og)をビルドし、マーカーrectがHTTPレスポンスにレンダリングされることを要求し、次に修正済みアプリ([email protected])をビルドし、その不在を要求します。
脆弱なコントロール:
[*] #FF0000 pixels: 75000 (threshold 10000)
[!!!] INJECTION CONFIRMED: the attacker value escaped <title> and
became a rendered SVG element in the generated image.
RESULT: VULNERABLE CONTROL CONFIRMED
修正済みコントロール:
[*] #FF0000 pixels: 0 (threshold 10000)
[=] no injection observed
RESULT: PATCHED CONTROL CONFIRMED
./lab start vulnerable # 16.3.5 app on http://127.0.0.1:9481/og
./lab test # assert injection renders
./lab start patched # 16.3.6 app
./lab test # assert absence
./lab status
./lab reset
スキャナを手動で実行する(デフォルトはループバック。許可された外部ターゲットには明示的なオーバーライドが必要):
python3 poc/scan.py http://127.0.0.1:9481/og
python3 poc/scan.py https://host.example/og --param value \
--allow-authorized-non-loopback [--insecure]
ペイロードは、500x150の純赤 <rect> を含むバランスの取れたXMLです。脆弱なエンドポイントでは、値が <title> を脱出し、rectが埋め込みSVGドキュメント内の実際の要素になります。スキャナは返されたPNG内の正確な #FF0000 ピクセルをカウントします(期待値75,000、閾値10,000)。修正済みエンドポイントでは、値はエスケープされたテキストであり、赤はレンダリングされません。プローブはターゲット上で何も変更しません。1回のGETリクエスト、1回の画像レンダリングのみです。
キャリアが意図的に <rect> であり、テキストではないことに注意してください。埋め込みSVG内のテキストはレンダラーにロード可能なフォントがなく、注入された場合でも不可視になるからです。
この脆弱性の証明はレンダリングされたラスタのピクセル内容であり、nucleiのマッチャはPNGをデコードできません。サイズ差分テンプレートを構築して却下しました。修正済みアプリでは、エスケープされたペイロードが依然として(リテラルの不活性テキストとして)レンダリングされるため、良性とペイロードのサイズは修正済みシステムでも異なり、脆弱と修正済みの差分の分離はアプリ固有の圧縮ノイズになります。修正済みシステムで発火するマッチャを提供するのは、何も提供しないより悪いです。poc/scan.py を使用してください。これはレスポンス画像を正確に評価します。
| バージョン | 評価 |
|---|---|
| Next.js 16.3.5 (vendored @vercel/og) | HTTP経由でインジェクションをエンドツーエンドで再現 |
| Next.js 16.3.6 (vendored @vercel/og) | 修正済みネガティブコントロールを再現 |
| satori 0.25.0 (npm) | シリアライズされたSVG内の生のインジェクション(ソースレベル) |
| satori 0.33.5 (npm, 2026年9月22日) | エスケープ済み(ソースレベル) |
| Next.js 16.2.0-16.3.4 | アドバイザリにより影響を受ける。個別には未テスト |
| Next.js 15.x | ベンダーブログによりRCE問題の影響を受けない |
.
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── app/
│ └── server.mjs # advisory route pattern over loopback HTTP
└── poc/
└── scan.py # non-destructive scanner (Python stdlib: urllib + zlib)
コミットされていないもの: assets/(npmでパックされたnext tarball + 展開されたバンドル)、node_modules/、.lab-state。
| 主張 | ステータス |
|---|---|
| satori < 修正 におけるSVGシリアライゼーションインジェクション | 証明済み(ソース + 動作) |
| next/og 16.3.5 経由の注入マークアップのエンドツーエンドレンダリング | 証明済み |
| 修正済みネガティブコントロール(16.3.6 / satori 0.33.5) | 証明済み |
| 属性/スタイルキャリア(同一の根本原因) | 根本原因は修正差分で確認済み。テキストキャリアは証明済み |
| 注入されたリソース参照経由のSSRF / ファイル読み取り | 未テスト |
| リモートコード実行 | 主張せず(未公開の下流パーサー詳細に依存) |
このリポジトリは、自身が所有するか、明示的にテストを許可されたシステムでのみ実行してください。ラボドライバは vulnerable/patched 以外のバリアントを拒否し、スキャナは --allow-authorized-non-loopback が指定されない限り非ループバックターゲットを拒否し、Composeポートは 127.0.0.1 にバインドされています。
開示の取り扱いについては SECURITY.md を参照してください。このリポジトリには、本番ターゲット、顧客データ、アクセストークン、実際のシステムからの証拠は含まれていません。
MIT - LICENSE を参照。