# Pillowの`open()`、`verify()`、`load()`をセキュリティ観点で理解する 画像アップロードは「画像ファイルを受け取る」だけではなく、**外部入力をデコーダ(パーサ)に通す**作業です。そこでPillowの3つのメソッドは、機能説明よりも**いつ呼び出すか(=攻撃表面をいつ開くか)**が重要です。 --- ## `open()`は「ピクセルを上げる関数」ではない `Image.open()`は**遅延**動作です。つまり、ファイルを「開いて識別」だけし、**ピクセルデータはまだ読み込まない**ことがあります。また、ファイルハンドルが開いたまま残ることもあります。 セキュリティ/運用で`open()`をうまく使う方法はシンプルです。 * `open()`でフォーマット識別、幅/高さなど**軽量情報**を取得 * ポリシーでブロック:許可フォーマット、最大解像度/ピクセル数、アップロード容量制限 * 次の段階(検証/デコード)へ進む つまり、`open()`は「デコード前段階で判断できる情報を抽出するツール」として安全に使うべきです。 --- ## `verify()`は何を保証し、何を保証しないか Pillowの`verify()`は「ファイルが壊れているか」を確認しようとしますが、**実際の画像データをデコードせずに検査**します。問題があれば例外を投げ、`verify()`後に画像を使うには**ファイルを再度開く**必要があります。 ここでのセキュリティ的結論は2点です。 * **メリット**:デコード(=重い作業)を避けつつ「壊れたファイル」を高速に除外できる * **限界**:`verify()`通過は「安全」ではなく「今すぐ大きく壊れていない」に近い。デコードを完了しないため、`load()`時点で問題が表れる可能性があります。 --- ## `load()`は検証段階で無闇に呼び出すと危険 `load()`は実際に**デコード(圧縮解凍を含む)を行い、ピクセルをメモリに上げる段階**です。この点が直ちにDoS(リソース枯渇)攻撃の表面になります。見た目のファイルサイズが小さくても、デコード結果が非常に大きくなる可能性があります。 Pillowは「デコンプレッションスプリング」リスクを警告/例外で扱い、デフォルトの閾値(例:128Mpx程度)などの保護機構を持っています。 Djangoも同じ理由で、画像アップロード検証で`load()`ではなく`verify()`を使用します。ソースには**「load()は全画像をメモリに上げてDoSベクターになる」**というコメントがあり、実際に`Image.open()`後に`verify()`を呼び出します。 --- ## Django/DRF使用時:`ImageField`で`verify()`をもう一度呼ぶと重複になる Djangoのフォーム`ImageField`検証は内部で`Image.open()` + `verify()`を実行します。DRFの`serializers.ImageField`も「Django実装に委譲する」コメント付きでDjango側検証を呼び出すフローを持ちます。 したがってDRFで`serializers.ImageField`を既に使っている場合: * ただ「壊れたか確認」だけで`validate()`内で`verify()`を再度呼ぶのは**ほぼ重複作業**になります。 * ビジネス検証/追加セキュリティ検証を強くカスタマイズしたい場合は、**`ImageField`ではなく`FileField`で受け取り**(検証パイプラインを直接設計)コストと責任を明確にする選択がよりクリーンです。 --- ## ユーザーがアップロードしたファイルに「安全」を確保するには ![アップロードファイルを安全に処理する図](/media/editor_temp/6/489648ac-eb83-4132-808b-f3ce0e07c366.png) 最も現実的な答えはこれです。 **「アップロード元をそのまま使わず、サーバーがデコードして新ファイルとして再保存した結果のみを使用する」** * `open()`で幅/高さ/フォーマットなど**低コスト情報**を読み、ポリシーで一次ブロック * `verify()`で**明らかに壊れたファイル**を除外 * 次に(通過したもののみ)制限された環境でデコードし**RGB/RGBAなど標準ピクセルへ正規化** * サーバーが選択したフォーマットで**再エンコードして新ファイル生成** * サービスは**サーバーが再生成したファイルのみ**を保存/配信 この戦略のメリットは「サーバーが最終出力の形を制御できる」点です。元ファイルにあった不要メタデータや奇妙な構造を大部分除去できます。 ただし再エンコードは結局`load()`に相当するデコードを含みます。 したがって**ピクセル数/メモリ制限(デコンプレッションスプリング防御)**などのガードレールを先に設定し、可能ならワーカー/隔離プロセスで実行するのが安全です。 --- ## まとめ * `open()`:**識別 + 低コスト情報確認**段階(ピクセルはまだないかもしれない) * `verify()`:**壊れたかの一次フィルタ**(デコードなしで検査、以降使用するには再オープン) * `load()`:**デコード/メモリ使用が始まる点**(検証段階での乱用は禁物) * アップロード安全の実務回答:**サーバーが再エンコードした結果のみを信頼**(ただし制限/隔離必須) **関連記事**: - [開発者が見る画像ファイルの姿は?画像ファイルを分解してみよう](/ko/whitedec/2026/1/14/developer-view-image-file-common-structure/) - [Django画像アップロードセキュリティガイド:サーバーが落ちないように効率的に処理する](/ko/whitedec/2026/1/13/django-image-upload-security-guide/) - [python-magic:拡張子ではなくファイル内容を信頼する最も実用的な方法](/ko/whitedec/2026/1/15/python-magic-file-type-detection/)