Axios: HTTP/2アダプターは、設定されたDNSルックアップやプロキシ制御を回避します。
## 概要
Node.js版Axiosでは、リクエストが`httpVersion: 2`を使用する場合、設定されたDNSルックアップやプロキシ制御が適用されません。HTTP/1アダプターは、`config.lookup`をラップして転送し、通常のHTTPリクエストオプションを構築し、`setProxy()`を通じてプロキシルーティングを適用します。一方、HTTP/2アダプターは、`http2.connect()`を使用してセッションを構築しますが、この際、上位レベルの`lookup`、`agent`、およびプロキシの状態は無視されます。
この問題は、アプリケーションがユーザーがリクエストの宛先を制御できるようにし、AxiosのHTTP/2を有効にし、かつAxiosの`lookup`またはプロキシルーティングを、SSRF(サーバーサイドリクエストフォージェリ)を防止したり、アウトバウンドのネットワークポリシーを強制するために使用している場合に影響します。
## 影響
影響を受けるサーバーサイド環境では、攻撃者がAxiosが、設定されたDNSリゾルバーやプロキシが拒否する宛先に直接接続するように仕向ける可能性があります。到達可能なサービスによっては、クラウドのメタデータ、内部サービスの応答が公開されたり、内部システムに対する状態を変更するリクエストが許可される可能性があります。
これは、すべてのAxios環境で必ずSSRFが発生するわけではありません。`httpVersion: 2`が有効であり、アプリケーションレベルで、ユーザーが操作するURLが`lookup`またはプロキシポリシーによって制限されている場合に発生する可能性があります。
## 影響を受ける機能
影響を受けるもの:
- `httpVersion: 2`が設定されたNode.js HTTPアダプター。
- DNSポリシーとして提供される`config.lookup`。
- HTTPS HTTP/2リクエストに対する、明示的に設定された`config.proxy`および環境変数から取得されるプロキシ設定。
影響を受けないもの:
- ブラウザ版アダプター。
- Node HTTP/1リクエスト。これは、`lookup`をトランスポートに渡し、プロキシ処理を適用します。
- Axiosを呼び出す前に、宛先ホストを独立して検証するアプリケーション。
## 技術的な詳細
`lib/adapters/http.js`では、アダプターが`lookup`を読み込み、それをラップして、リクエストの`options`に格納し、トランスポートを選択する前に`setProxy()`を適用します。HTTP/2の場合、`http2Transport.request()`は、`options.protocol`、`options.hostname`、および`options.port`から権限を構築し、その後、以下の処理を実行します。
```js
const { http2Options, headers } = options;
const session = http2Sessions.getSession(authority, http2Options);
```
`lib/helpers/Http2Sessions.js`は、最終的に`http2.connect(authority, options)`を呼び出しますが、引数として`http2Options`オブジェクトのみが渡されます。上位レベルのリクエストオプションで設定されたDNSルックアップやプロキシ、またはトンネリングエージェントは、この呼び出しには引き継がれません。
axiosの`1.18.1`でのローカル検証では、`lookupCalls: 0`という結果が得られましたが、ローカルのHTTP/2サーバーへのリクエストは成功しました。別のローカルHTTPS HTTP/2検証で、明示的にリクエストを拒否するHTTPプロキシを使用した場合、オリジンサーバーがリクエストを受信し、プロキシがトラフィックを検知しないことが確認されました。
## 攻撃の概念実証
ローカルでの限定的なデモンストレーション:
1. ループバックインターフェース上でHTTP/2サーバーを開始します。
2. `axios.get("http://localhost:<port>/internal", { httpVersion: 2, lookup })`を呼び出します。ここで、`lookup`は`EPOLICY`エラーを発生させます。
3. リクエストが成功し、ルックアップカウンタが`0`のままになっていることを確認します。
プロキシルーティングの場合:
1. HTTPS HTTP/2オリジンサーバーと、すべてのリクエストとCONNECTを拒否するローカルHTTPプロキシを開始します。
2. axiosを`httpVersion: 2`、`http2Options: { rejectUnauthorized: false }`、および明示的な`proxy`オプションとともに呼び出します。
3. オリジンサーバーがリクエストを受信し、プロキシが何も受信しないことを確認します。
期待される安全な動作は、DNSルックアップポリシーによってリクエストがブロックされるか、プロキシがリクエストを検知して拒否することです。
## 対策
axiosの`lookup`機能やプロキシ制御に依存するリクエストについては、HTTP/1アダプターを使用してください。あるいは、axiosを呼び出す前に、宛先に対する許可/拒否ポリシーを適用してください(アダプターのトランスポート層の外側で)。
<details>
<summary><h3>元のレポート</h3></summary>
### 概要
Axiosが、リクエストで`httpVersion: 2`を使用する場合、設定された`lookup`関数やプロキシを適用しないことがわかりました。HTTP/1アダプターは、これらの両方の制御を適用しますが、HTTP/2のパスは`http2.connect()`を使用して、URLのホスト名に直接接続します。
これは、ユーザーからの入力に基づいてURLを受け取り、SSRF(サーバーサイドリクエストフォージェリ)を防止するためにカスタムDNSルックアップや強制的なアウトバウンドプロキシを使用するサーバーアプリケーションにとって重要な問題です。AxiosインスタンスをHTTP/2に切り替えると、これらの制御が静かに無効になり、リクエストがアプリケーションがブロックすることを意図したアドレスに到達する可能性があります。
私は、現在のnpmリリース版であるAxios 1.18.1でこの問題を再現しました。
### 詳細
HTTPアダプタは、呼び出し元の`lookup`関数を読み込み、それをラップし、通常の要求オプションを構築し、`setProxy()`を呼び出します。
- `lib/adapters/http.js`、530行目付近~578行目付近:`lookup`を読み込み、ラップします。
- `lib/adapters/http.js`、895行目付近~954行目付近:`lookup`を`options`に追加し、`setProxy()`を適用します。
しかし、HTTP/2の場合、アダプタは`http2Transport`を選択します。このトランスポートは、宛先から権限を作成し、`options.http2Options`のみをセッションプールに渡します。
```js
const { http2Options, headers } = options;
const session = http2Sessions.getSession(authority, http2Options);
```
次に、`lib/helpers/Http2Sessions.js`が以下を呼び出します。
```js
const session = http2.connect(authority, options);
```
この時点では、`options`は`http2Options`オブジェクトのみが含まれています。トップレベルの`lookup`、`setProxy()`によって作成されたプロキシトンネリングエージェント、および選択された`httpAgent`/`httpsAgent`は、転送されません。したがって、リクエストはシステムリゾルバを使用し、宛先に直接接続します。
この根本原因は、明示的なプロキシ設定と、環境変数から取得したプロキシ設定の両方に影響を与えます。私は、この問題を再現するための最小限のコード(PoC)をローカル環境で実行し、明示的なプロキシを使用することで、結果がシェル環境変数に依存しないようにしました。
### PoC
私は、[axios_http2_transport_controls_poc.mjs](https://drive.google.com/file/d/1lKXBOLDoysOFRmaRyV1tMVLx3P0Ah4gV/view?usp=sharing)を添付しました。このPoCは完全にローカル環境で実行され、以下の3つの要素を設定します。
1. HTTP/2を有効にしたHTTPSオリジン。アクセスされると、リクエストを記録し、`REACHED_BLOCKED_ORIGIN`を返します。
2. トラフィックを記録するHTTPプロキシ。通常のすべてのリクエストと、`502 Bad Gateway`のエラーを返すすべてのCONNECTリクエストを拒否します。
3. カスタムのAxios `lookup`コールバック。すべてのDNSルックアップを`EPOLICY`エラーで拒否します。
最初のリクエストは、HTTP/1のコントロールリクエストです。このリクエストは、ブロックする`lookup`コールバックを使用しており、プロキシが無効になっています。Axiosはコールバックを呼び出し、`EPOLICY`を受け取り、オリジンに到達しません。これにより、コールバックが機能しており、ホスト名が通常のアダプターパスを通じてブロックされていることが確認されます。
2番目のリクエストは、同じURLを`httpVersion: 2`でターゲットにしています。このリクエストには、ブロックする`lookup`コールバックと、リクエストを拒否するプロキシという、2つのセキュリティコントロールが適用されます。Axiosがどちらか一方でも適用される場合、このリクエストはオリジンに到達できません。`EPOLICY`エラーで失敗するか、プロキシに到達し、その`502`レスポンスを受け取るはずです。
しかし、実際には、このリクエストはHTTP 200を返し、`REACHED_BLOCKED_ORIGIN`が含まれています。ルックアップカウンタは増加せず、プロキシはリクエストやCONNECTの試行を記録していません。オリジンは、トラフィックを記録する唯一のサーバーです。これは、HTTP/2のパスが両方のコントロールをスキップし、システムリゾルバーを使用して直接接続したことを示しています。
再現するには、Axiosのソースコードのルートディレクトリから以下のファイルを実行します。
Axiosのソースコードのルートディレクトリから実行します。
```bash
git checkout v1.18.1
npm install --ignore-scripts
node /path/to/axios_http2_transport_controls_poc.mjs
```
私の実行結果からの関連部分:
```json
{
"axiosVersion": "1.18.1",
"configuredControls": {
"lookup": "すべてのDNSルックアップをEPOLICYで拒否",
"proxy": "http://127.0.0.1:<ポート> (すべてのリクエストを拒否)"
},
"http1Control": "EPOLICY: アプリケーションのDNSポリシーによってブロックされています",
"http2Result": {
"status": 200,
"data": "REACHED_BLOCKED_ORIGIN"
},
"lookupCalls": 1,
"proxyObservedTraffic": false,
"events": [
{
"server": "origin",
"protocol": "h2",
"path": "/internal"
}
]
}
```
出力の重要な部分は以下の通りです。
- `http1Control` には `EPOLICY` が含まれており、HTTP/1でDNSポリシーが宛先をブロックしていることがわかります。
- `lookupCalls` は両方のリクエスト後も `1` のままであり、HTTP/2が設定されたリゾルバを呼び出していないことがわかります。
- `proxyObservedTraffic` は `false` であり、HTTP/2が設定されたプロキシを使用していないことがわかります。
- `http2Result.status` は `200` であり、唯一のイベントはオリジンに属しており、Axiosがブロックされた宛先に直接接続したことがわかります。
### 影響
脆弱な設定は、以下の機能を備えたNode.jsアプリケーションです。
- `httpVersion: 2` を使用してAxiosのHTTP/2を有効にします。
- アプリケーションのユーザーがリクエストの宛先を制御できるようにします。
- 宛先またはエグレスポリシーを適用するために、Axiosの `lookup` オプションまたはプロキシルーティングに依存します。
この設定では、認証されていないアプリケーションのユーザーが、ルックアップポリシーまたはプロキシによって拒否されるはずの、ローカル、プライベートネットワーク、またはリンクローカルサービスにサーバーが直接接続するように操作できる可能性があります。 アクセス可能なサービスによっては、クラウドの認証情報や内部データが公開されたり、内部サービスが変更されたり、可用性に影響が出たりする可能性があります。
HTTP/2のサポートは実験的なものとしてマークされていますが、HTTP/2のドキュメントにもプロキシのドキュメントにも、`lookup` とプロキシの制御が無視されるという記述はありません。 プロキシのドキュメントには、HTTPSリクエストはCONNECTトンネルを通じて送信されると記載されています。 さらに重要なのは、Axiosの脅威モデルでは、宛先の検証は呼び出し元の責任であると示されており、この動作は、そのような呼び出し元が提供する検証を黙ってバイパスします。
私は、サードパーティ製のサービスや本番環境での動作を検証していません。このPoC(概念実証)は、私のローカル環境でのみ動作します。
---
影響を受けるパッケージ:
- npm (JavaScript) axios: 影響を受けるバージョンは 1.13.0 以上、1.20.0 未満。1.20.0 で修正済み。