tornado: `CurlAsyncHTTPClient`はレスポンスサイズに制限を設けていません。そのため、圧縮爆弾によってメモリが無限に増加し、最終的にメモリ不足(OOM)を引き起こす可能性があります。
`tornado.curl_httpclient.CurlAsyncHTTPClient`において、CVE-2026-49855のクライアント側の関連脆弱性として、メモリの無限な増加(圧縮爆弾)が発生します。これは、2026年8月15日のmaster版(`6.6.dev1`)で確認され、最新のリリース版`v6.5.8`および現在のmaster版(2026年8月17日確認)でも同様の問題が確認されています。Tornadoアプリケーションがcurlクライアントを設定し(プロキシサポートや高度なTLSオプションのための推奨設定)、攻撃者が指定したURLまたは攻撃者に改ざんされたURLに対して、デフォルト設定で`decompress_response=True`とした状態で`fetch()`を実行すると、悪意のあるサーバーが`Content-Encoding: gzip`で約2.8MBのデータを送信することで、クライアントのメモリ使用量が30,884 kBから**1,032,100 kB(約1008MB)まで3.18秒で増加**しました(約350MB/s、単調増加で、プラトーは見られません)。最終的に、カーネルがプロセスを強制終了(exit 137、cgroup `OOMKilled=true`)しましたが、その時点でのデータ転送は67%しか完了しておらず、**クライアント側でのサイズチェックは一切行われていません**。`curl_httpclient.py`には、`max_body_size`または`MAXFILESIZE`という文字列は一切含まれていません。同様の攻撃をデフォルトの`SimpleAsyncHTTPClient`に対して行うと、約65MBで正常に処理が停止します。これは、CVE-2026-49855で追加されたすべてのサイズ制限(圧縮されたコンテンツ、チャンク化されたデータ、累積された解凍サイズ — `http1connection.py:620,676,742`)が、curlクライアントが実行しないコードに存在するためです。これは、公開されているアドバイザリ(`_GzipMessageDelegate`または`SimpleAsyncHTTPClient`のみを修正)や、他のcurlクライアント関連のアドバイザリ(認証情報再利用に関するGHSA-pw6j-qg29-8w7f、ヘッダーのCRLFに関するGHSA-w235-7p84-xx57)とは異なる問題であり、このファイルの完全なコミット履歴(最終更新は2026年6月17日)を確認しても、レスポンスサイズに関する修正は見当たりません。
## 詳細
`tornado/curl_httpclient.py`(行番号はmaster版`6.6.dev1`、`v6.5.8`、および監査対象のバージョンで同一):
```python
"buffer": BytesIO(), # :202 — 通常のBytesIOオブジェクト。サイズ制限なし
...
else:
write_function = buffer.write # :359 — 圧縮されたすべてのバイトがここに書き込まれる
curl.setopt(pycurl.WRITEFUNCTION, write_function) # :360
...
if request.decompress_response: # デフォルトはTrue(HTTPRequestの場合)
curl.setopt(pycurl.ENCODING, "gzip,deflate") # :373-374 — libcurlが自動的に解凍する
```
libcurlは、`WRITEFUNCTION`を呼び出す**前に**レスポンスを解凍するため、コールバック関数は解凍されたバイトを受け取ります。これらのバイトは、転送が終了するかプロセスが終了するまで、サイズ制限のない`BytesIO`オブジェクトに追加されます。唯一の制限は`request_timeout`(デフォルトは20秒)です。zlibの速度は数百MB/sに達するため、それでも数GBのデータが蓄積される可能性があります。PoCではこの値を300秒に設定しましたが、それでも1 GiBのcgroup制限は3.2秒で到達しました。`pycurl.MAXFILESIZE`や、`max_buffer_size`/`max_body_size`に関する機能はありません(コンストラクタはボディサイズのオプションを受け入れません)。また、`streaming_callback`を使用しても状況は改善されません(`:353-356`のコールバック関数も、サイズに関する処理は一切行いません)。
対照的に、`SimpleAsyncHTTPClient`のパス(`tornado/http1connection.py`)には、curlのパスには存在しない以下の機能があります。
```python
if cast(int, content_length) > self._max_body_size: # :620 Content-Lengthによる制限
if total_size > self._max_body_size: # :676 チャンク化されたデータの合計サイズによる制限
if self._decompressed_body_size > self._max_body_size: # :742 CVE-2026-49855による解凍後のサイズ制限
```
`SimpleAsyncHTTPClient.initialize()`では、`max_buffer_size`がデフォルトで104857600(100 MiB)に設定され、`max_body_size`もそれにデフォルトで設定されます(`simple_httpclient.py:117-121`)。一方、`CurlAsyncHTTPClient.initialize()`には、対応するパラメータは一切存在しません。
攻撃のシナリオ(攻撃者:悪意のあるHTTPサーバー;被害者:攻撃者の影響下にあるURLに対して`fetch()`を実行するTornadoアプリケーション — URL取得機能、Webhookプロセッサ、リンクプレビュー機能、RSS/プローブポーリング機能など):
1. アプリケーションは `AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")` を設定します(プロキシサポートに関するドキュメントを参照。プロキシは curl クライアントでのみサポートされます)。
2. アプリケーションはデフォルト設定で `fetch("http://attacker/...")` を呼び出します → リクエストは `Accept-Encoding: gzip,deflate` を送信します。
3. 攻撃者は `200`、`Content-Encoding: gzip`、`Transfer-Encoding: chunked` を返します。ボディはゼロの gzip ストリームで、ワイヤ上で 4,174,525 バイト(展開すると 4 GiB)で、1029:1 の比率で、64 KB チャンクで送信されます。
4. libcurl は約 350 MB/s で自動的にデコードし、サイズを考慮せずに `buffer.write` に書き込みます → プロセスの RSS が線形に増加し、最終的にメモリ不足 (OOM) になります。レスポンスが完了する必要はありません。クライアントは 2,818,048/4,174,525 バイト(67%)を送信した時点で停止しました。
圧縮なしのバリエーション:`decompress_response=False` を設定し、無限にストリーミングされるボディ(Content-Length がなく、最終チャンクもない)を同じようにサイズを考慮せずに `buffer.write` に書き込みます。このクライアントは、どのパスに対しても制限を強制しません。
## PoC (概念実証)
2026年8月15日に、単一のコンテナ(cgroup `--memory 1g --memory-swap 1g` で、メモリ枯渇のエンドポイントは安全で、観察が容易)内で、実際の TCP ソケット `127.0.0.1:8081` 上で、2つのプロセスで検証しました。プロセスは、raw-socket を使用した悪意のあるサーバー (`evil_server.py`、4 GiB のゼロの gzip ボムを一度だけ作成し、チャンク化されたフレームで送信します)と、Tornado の被害者 (`victim_curl.py`、`CurlAsyncHTTPClient` を設定し、`fetch(..., request_timeout=300)` を呼び出し、監視スレッドから `/proc/self/status` の VmRSS を 0.2 秒ごとにサンプリングし、OOM キルが発生するまで RSS の変化を監視します)。
### 被害者のビルドと実行
```bash
git clone https://github.com/tornadoweb/tornado
cd tornado
pip install pycurl
python evil_server.py & # 127.0.0.1:8081; BOMB_BUILT wire_bytes=... ratio=... EVIL_LISTENING を出力
python victim_curl.py # 被害側: CurlAsyncHTTPClient fetch -> RSS が線形に増加することを期待 -> OOM
python victim_simple.py # 制御: 同じボムに対して、デフォルトの SimpleAsyncHTTPClient を使用
```
検証環境:debian:bookworm-slim、Python 3.11.2、python3-pycurl 7.45.2 (libcurl 7.88.1、zlib 1.2.13)、Tornado master snapshot `6.6.dev1` (2026年8月15日)、ソースコードから実行 (`sys.path`)、コンテナのメモリ制限は1 GiB。`curl_httpclient.py` は、タグ `v6.5.8` および 2026年8月17日現在の master ブランチで、バイト単位での同等性が確認されています(サイズ制限に関する参照は依然としてゼロ)。
### 再現手順
1. **前提条件 — curlクライアントを使用してリモートURLを取得する、ドキュメントに記載されている構成。** この脆弱性は、アプリケーションが `CurlAsyncHTTPClient` (プロキシサポートや高度なTLSオプションが必要な場合の標準設定) を、デフォルトの `decompress_response=True` で使用し、攻撃者が制御または侵害したサーバーのURLを取得している場合に発生します。`victim_curl.py` はまさにその動作を実装しています (`AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")` → `fetch()`)。サーバーのログは、ENCODING パスが使用されたことを確認しています。被害者のリクエストは、`User-Agent: Mozilla/5.0 (compatible; pycurl)` および `Accept-Encoding: gzip,deflate` というヘッダーで送信されました。
2. **攻撃:** `evil_server.py` を起動し、`EVIL_LISTENING` を待ち、次に `victim_curl.py` を実行します(取得自体が攻撃であり、それ以上の操作は不要です)。
3. **期待される結果:** `victim_curl_rss.log` には、VmRSS が 30,884 → 1,032,100 kB となり、3.18秒で約350 MB/s の速度で増加し、プラトーにならないことが表示されます。コンテナが被害者を停止します (`exit 137`、docker `OOMKilled=true`)。サーバーのログには、`CLIENT_DIED_MID_TRANSFER wire_sent=2818048/4174525` と記録されます。この累積は、利用可能なメモリによってのみ制限され、クライアント側のチェックによる制限はありません。4 GiB のペイロードは完全に送信されませんでした。
4. **バリエーション:** `decompress_response=False` を設定し、チャンク化されたボディが終了しない場合(サーバーが永遠に送信し続ける場合)、同じ `buffer.write` パスで、圧縮が不要なプレーンなバイトが無限に累積されます。`request_timeout` は、上限を延長するだけです。
5. **制御:** `victim_simple.py` は、デフォルトの `SimpleAsyncHTTPClient` を使用して同じ爆弾を取得します → `FETCH_FAILED: HTTP 599: Connection closed` が表示され、RSS のピークは約 65 MB (24,984 → 66,712 kB)、スクリプトは正常終了 (exit 0)。`http1connection.py` 内の CVE-2026-49855 に関連する処理が転送を中断し、この問題が curl クライアントに特有であることを示しています。
### PoC ソースコード
完全な PoC ソースコード (**victim_curl.py**、標準ライブラリのみ、依存関係なし): [victim_curl.py](https://gist.github.com/afldl/211c0e7af8175c2eabab53ed037c435d) (非公開の gist)。
この gist には、`evil_server.py` (爆弾サーバー)、`victim_curl.py` (被害側)、`victim_simple.py` (制御)、および `REPRODUCE.md` が含まれています。
### キャプチャされた出力 (2026年8月15日の実行、完全な抜粋)
```
evil_server.log:
BOMB_BUILT wire_bytes=4174525 decompressed_bytes=4294967296 ratio=1029:1
EVIL_LISTENING 127.0.0.1:8081
REQUEST_FROM 127.0.0.1:57544 -> GET /bomb HTTP/1.1
REQUEST_HEADERS:
GET /bomb HTTP/1.1
Host: 127.0.0.1:8081
User-Agent: Mozilla/5.0 (compatible; pycurl)
Accept: */*
Accept-Encoding: gzip,deflate
WIRE_SENT 65536/4174525
WIRE_SENT 2162688/4174525
CLIENT_DIED_MID_TRANSFER wire_sent=2818048/4174525 err=ConnectionResetError(104, 'Connection reset by peer')
victim_curl_rss.log (VmRSS kB, every 0.2 s):
0.00 30884
0.61 231848
1.41 514720
2.22 794480
2.82 1000756
3.18 1032100 <- kill される直前の最終サンプル
driver_f1.log:
timeout 240 python3 /e2e/F1/victim_curl.py ... 606 Killed
VICTIM_CURL_EXIT=137 # docker inspect -> OomKilled: true
victim_simple.log (制御、同じ爆弾):
FETCH_FAILED: HTTP 599: Connection closed
SCRIPT_FINISHED_NORMALLY # 正常終了 (exit 0)、VmRSS のピーク 66712 kB
```
この問題の状況を正確に説明すると、約 1008 MB で発生した OOM (Out of Memory) エラーは、テストで使用されている 1 GiB の cgroup の制限によって強制的に発生したものです。この「無制限」という表現は、線形に増加しプラトーにならない RSS のグラフ、転送量の 67% でのクラッシュ、およびコード内のサイズに関する処理がないことに基づいています。より多くのメモリがあれば、転送は 4 GiB のペイロード全体が完了するまで継続されていた可能性があります。
## 影響
curl HTTPクライアントを使用し、攻撃者によって制御されたURLをフェッチするTornadoアプリケーションにおいて、サービス拒否(メモリ枯渇)が発生する可能性があります。約4MBの応答が、約3秒で1GiBのプロセスを停止させます。メモリ消費量は、`available_memory / 1000`に比例します。この問題は、共有イベントループのクライアントで発生するため、1つの悪意のある応答がアプリケーション全体(同時にサービスを提供しているすべてのユーザー)を停止させます。また、徐々にメモリを消費するタイプの攻撃では、検出される前にメモリが不足する可能性があります。この攻撃には、特権も、ユーザーの操作も必要ありません。必要なのは、被害者の設定されたクライアントが、攻撃者のサーバーにアクセスすることだけです。
### 推奨される対策
`CurlAsyncHTTPClient`の書き込みパスにおいて、バイト数の制限を設けます。具体的には、`WRITEFUNCTION`(`buffer.write`の分岐と`streaming_callback`の分岐の両方)を、受信した合計データ量が`max_body_size`を超えた場合に転送を中断するカウンターでラップします(`curl.setopt(pycurl.FAILONERROR)`のようなキャンセル処理、またはコールバックからの例外発生)。`max_body_size`は、`initialize()`メソッドで設定され、`SimpleAsyncHTTPClient`と同じデフォルト値である100MiBです。libcurlは、書き込みコールバックの前にデータを展開するため、カウンターは自然と展開されたバイト数を計測します。これは、CVE-2026-49855の修正と同様の動作です。(`pycurl.MAXFILESIZE`だけでは不十分です。これは、圧縮された転送サイズに適用されます。)
### 影響を受けるバージョン
- **<= 6.5.8**(最新のタグ。curlクライアントには、応答サイズの制限はありません)およびmasterブランチ(`6.6.dev1`、2026年8月15日/17日に検証済み)。
- 6.5.6のCVE-2026-49855の修正は、`SimpleAsyncHTTPClient`/`http1connection.py`のみを対象としており、`curl_httpclient.py`は変更されていません。
## 報告者
diff/ambidiffセキュリティ研究グループ(afldl)。
影響を受けるパッケージ:
- pip (Python) tornado: 影響を受けるバージョンは6.5.8以下、6.5.9で修正済み。