Pillow: 11.1.0以降のすべてのパッケージにlibopenjp2 2.5.4が同梱されており、Image.open().load()から到達可能なOSV-2025-219というヒープバッファオーバーフローの脆弱性があります。
### 概要
11.1.0以降のすべてのPillowパッケージに同梱されているlibopenjp2には、アップストリームでは修正済みですが、OpenJPEGのリリース版には含まれていないヒープバッファオーバーフローの脆弱性(書き込み)があります。詳細は、https://osv.dev/vulnerability/OSV-2025-219 を参照してください。
このパッケージが独自のコピーを同梱しているため、ホストOSの修正だけでは解決できません。デフォルト設定で、`Image.open(fp); im.load()` を実行することで、この脆弱性が利用される可能性があります。
この問題を、Pillowのアップストリームではなく、ここで報告するのは、脆弱なバイナリが実際にユーザーに届くのがPillowだからです。
注意すべき点は、**OpenJPEG 2.5.2以前のバージョンは安全である**ということです。このバグは `954c6e3c` (2024-06-25) で導入されたため、依存関係を最新の状態に保つことが、この脆弱性の原因となります。
### 詳細
## 影響を受けるパッケージ
`.github/dependencies.json` (openjpeg) の各バージョンにおける情報:
| Pillow |同梱されたOpenJPEG | 状態 |
|---|---|---|
| 10.4.0 | 2.5.2 | 安全 |
| 11.0.0 | 2.5.2 | 安全 |
| **11.1.0, 11.2.1, 11.3.0** | **2.5.3** | **影響あり** |
| **12.0.0 … 12.3.0** | **2.5.4** | **影響あり** |
インストールされたパッケージで確認した結果:`PIL.features.version_codec('jpg_2000')` → `2.5.4`、同梱ファイルは `pillow.libs/libopenjp2-*.so.2.5.4`。 2026-10-06現在も、`main` は `2.5.4` を指定しています。
## 脆弱性の詳細
`src/lib/openjp2/j2k.c` の `opj_j2k_read_sod` 関数内:
```c
OPJ_UINT32 l_current_tile_part =
l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
l_cstr_index->tile_index[...].tp_index[l_current_tile_part].end_header = l_current_pos;
l_cstr_index->tile_index[...].tp_index[l_current_tile_part].end_pos =
l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
```
`tp_index` はヌルチェックも境界チェックも行われておらず、`l_current_tile_part` はSOTマーカーの1バイトの `TPsot` フィールドから直接取得されます。
`opj_j2k_add_tlmarker`(j2k.c:8459)の数行後には、すでに正しいガードが存在し、`if (tp_index && l_current_tile_part < nb_tps)`の直後に、**同じ配列を同じインデックスで**書き込んでいます。ある開発者が確認しましたが、関連する箇所は確認されていません。
これは、コーデックストリームに*有効な*TLMマーカーを与えた場合に発生します。そのため、`tp_index`は`opj_j2k_build_tp_index_from_tlm`によって一度割り当てられ、TLMのエントリ数に合わせてサイズが設定され、`opj_j2k_read_sot`はすべてのリサイズをスキップします。また、すべてのSOTで`TNsot = 0`を設定することで、TLMが「有効」な状態を維持し、`TPsot`が割り当てられた領域を通過できるようになります。
## パッチの状況
`91d08b11`("Fix heap buffer overflow in opj_j2k_read_sod"、2026年2月10日)で、上流で修正されました。
最新のOpenJPEGのバージョンは**v2.5.4 (2025年9月20日)**で、`compare/v2.5.4...91d08b11`は、*前方6、後方0*と報告されています。つまり、この修正は**どのリリースにも含まれておらず、8ヶ月間放置されています**。
## 推奨される対応策
1. `91d08b11`を、OpenJPEGのリリースにそれが含まれるまでの間、wheel buildのパッチとして適用する。これは3行のコードで、同じファイルにすでに存在するガードと同一です。
2. OpenJPEGに、リリースを出すよう依頼することを検討する。彼らは2026年2月以降、マスターブランチで未リリースとなっているメモリ破壊の修正を持っていますが、すべてのディストリビューションが脆弱なコードを配布しています。
3. セキュリティハンドブックでは、すでにアプリケーションに対して、アプリケーションレベルでEPSをブロックするように指示しています。同様に、`Image.open()`がファイル名/Content-Typeを無視し、`formats=`が指定されない限り、コンパイルされたすべてのコーデックに到達する、という明示的な注意を促すことで、下流のアプリケーションにとって非常に役立ちます。
## Pillowのバグではないが、関連するもの:多くのアプリケーションが依存する拡張機能の許可リストは適用されない
`Image.open()`はマジックバイトをチェックしますが、`avatar.png`という名前のファイルにJPEG 2000のコーデックストリームが含まれており、`Content-Type: image/png`と宣言されている場合でも、JP2デコーダに到達してしまいます。
私は、PNG/JPEGのみのアップロードを許可する本番環境のサービスでこれを検証しました。そのサービスでは、特定のパスを通じてJP2ファイルが正常に受け入れられ、完全にデコードされました。そのパスからは16種類の形式が利用可能で、43種類のデコーダーが登録されていました。その後、そのサービスは、クライアントが送信した`Content-Type`ではなく、検出された形式に基づいて検証を行うように修正されました。
そのため、この種のデコーダーレベルの問題は、下流の担当者が想定するよりもはるかに広範囲に影響を及ぼす可能性があります。
### 実証コード
OpenJPEG 2.5.4のASanビルドを、**標準的なPillow 12.3.0**の下に配置し、`Image.open(p); im.load()`を実行した結果:
```
==766332==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
#0 opj_j2k_read_sod j2k.c:5086
#1 opj_j2k_read_tile_header j2k.c:9912
#2 opj_read_tile_header openjpeg.c:575
#3 j2k_decode_entry src/libImaging/Jpeg2KDecode.c:793
#4 ImagingJpeg2KDecode src/libImaging/Jpeg2KDecode.c:949
#5 _decode src/decode.c:132
上記のエラーは、
opj_j2k_build_tp_index_from_tlm j2k.c:8939
によって割り当てられた24バイトの領域の右側に8バイトの位置で発生しました。
```
フレーム3~5は、Pillow自体のデコーダーであり、テスト用のコードではありません。デフォルト以外のデコーダーオプションは一切使用されていません。厳格モードが有効になっており、これはPillowのデフォルトの動作です。Pillowは`opj_decoder_set_strict_mode`を呼び出しません。正常な動作をする類似の画像ファイルでも同じパスを通って正常に動作し、有効な画像が返されるため、これは解析エラーではなく、ペイロードの問題です。
私は、意図的にこの初期報告に実証コードのファイルを添付していません。ご要望に応じて、実証コード、ビルドスクリプト、およびRIP制御用のテストコードをここに添付することも可能ですし、修正が完了するまですべて保留することも可能です。
私は、この問題が解決されるまで、一切の情報を公開することはありません。
### 影響
`Image.open()` / `im.load()` を呼び出して、自身が生成していない画像ファイルを読み込むアプリケーションは、作成された特定のファイルを提供することで、バッファオーバーフローを引き起こす可能性があります。これは、アップロード処理、サムネイル生成、プレビュー機能など、多くのケースで発生します。
**下限値、すでに確認済み。** 標準的な、サニタイズされていない環境では、オーバーフローによって245回の書き込みが発生し、24バイトの領域を超えて+5856バイト先に進みます。その結果、glibcのヒープメタデータチェッカーがプロセスを停止させます(「破損したサイズと前のサイズの不一致」、「破損した二重リンクリスト」、SIGABRT)。特定のファイルを使用することで、長時間稼働しているプロセスを確実に停止させることができます。
**上限値、基本的な動作は確認済みですが、完全なエクスプロイトではありません。** 両側に同じサイズのオブジェクトが存在する、通常の混雑したヒープ環境では、この動作は `opj_event_mgr_t` を通過します。`opj_event_mgr_t` は3つの `opj_msg_callback` 関数ポインタを保持しており、`opj_event_msg` がそのうちの1つを呼び出します(event.c:136):
```
rip 0x4000 <- 攻撃者がファイルレイアウトで選択した値
rsi 0x3ff2 <- m_error_data、同じ動作によって書き込まれた値
```
COMマーカーを使用してデータストリームをずらすと、`rip` も一緒にずらされます(`0x16ce` → `0x4000`)。これにより、命令ポインタは偶然無効になるのではなく、攻撃者が**選択した**値になります。無害なコントロール画像を使用すると、ヒープの状態は変更されません。
私が実施しなかったことを明確に述べます。私は、動作するエクスプロイトを**作成していません**。これには、リモートヒープの操作と、ASLRの回避が必要です。私はどちらも試みませんでした。私は、完成された攻撃チェーンではなく、制御フローの基本的な動作を報告しています。CVSSのAC:Hは、この点を反映しています。
影響を受けるパッケージ:
- pip (Python) pillow: 影響を受けるバージョンは 11.1.0以上、13.0.0未満。13.0.0で修正済み。