本記事は連載「事故から逆算するWebセキュリティ」の第23回です。アップロード機能を持つWebアプリの開発者に向けて、ファイルアップロードで起きる事故がどこから生まれるか、そして中身での検証・保存先の設計・配信の仕方でどう防ぐかを、OWASPの指針と手元で動かした確認コマンドつきで整理します。セキュリティ全体の地図は「サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説」にまとめています。
UPLOAD PATH通り道と守り
危険はどこにどう保存し、どう配信するかで決まる
-
受け取る
サイズ・件数・MIMEは自己申告
-
中身で検証
マジックバイト+許可リスト
-
保存する
Webルート外・実行不可・名前はサーバー
-
配信する
別経由・Content-Disposition
30秒でわかる
・ファイルの種類は拡張子やブラウザが送るMIMEではなく、中身(ファイル署名)で確かめ、許可リストで絞る
・保存先はWebルートの外に、実行権限を付けずに置く。保存名はサーバーがUUIDなどで決め、元の名前をパスに使わない
・画像は再エンコードしてメタデータを落とす。サイズ・件数・レートに上限を置いてDoSを防ぐ
・配信はContent-Dispositionで添付扱いにし、SVGやHTMLは別オリジンか無害化。どれか一つでなく層で守る
ファイルアップロードで実際に起きる事故
アップロード機能の事故は、受け取ったファイルを信用しすぎたときに起きます。拡張子だけ見て中身を確かめず、Webルート直下に元の名前のまま置き、サーバーが実行できる形で配信する。この並びのどこかが欠けると、攻撃者の入り口になります。
危険は「どんなファイルを受け取るか」より、どこにどう保存し、どう配信するかで決まります。同じ1枚のファイルでも、画像として添付配信するだけなら無害で、サーバーがスクリプトとして実行できる場所に置けば実行の起点になります。同じ受け入れ条件でも、影響範囲は保存先と配信の設計で大きく変わるということです。本記事が入口の検証だけでなく保存と配信までを一続きで扱うのは、このためです。
弱いアップロードで攻撃者ができること(概念)
| アップロードできたもの | 起こりうること |
|---|---|
| 実行可能なスクリプト(拡張子を偽装) | サーバー上でコードが動く |
| scriptを含むHTML・SVG | 閲覧者のブラウザでXSS |
| 巨大・大量のファイル | ストレージ枯渇・処理の過負荷(DoS) |
../ を含む名前 |
想定外の場所への書き込み・上書き |
本記事は防御の設計を扱い、回避の具体的な手口やペイロードは載せません。
URLを渡してサーバーに取得させる「URL指定のアップロード」は、取得先を攻撃者が操れると内部ネットワークを叩かせるSSRFの面も持ちます。取得側の守りは「SSRFとは|サーバーに内側を叩かせる攻撃の仕組みと、素朴なブロックリストが破られる理由・正しいSSRF対策」に分けてあります。
拡張子でなく中身でファイルの種類を確かめる
最初の分かれ目は「種類の確かめ方」です。拡張子は利用者が自由に付けられ、ブラウザが送る Content-Type も自己申告にすぎません。OWASP File Upload Cheat Sheet は「アップロードされたファイルのContent-Typeは利用者が与えるもので、偽装が容易なので信用できない」と明言しています。
FIX THE DEFAULTSつい書く→直す
アップロードの事故はつい書く実装から生まれる
つい書く実装
直した設計
つい書く実装: 拡張子で種類を判定
直した設計: 中身で確かめ許可リスト方式
つい書く実装: ブラウザのMIMEを信用
直した設計: 自己申告は信用しない(偽装は容易)
つい書く実装: 元のファイル名で保存
直した設計: サーバーがUUID等で名前を決める
つい書く実装: Webルート直下に置く
直した設計: ルート外・実行権限なしで保存
やることは三つです。許可リスト方式で業務に必要な安全な拡張子だけを通し(ブロックリストは単体では弱い防御)、ファイルの先頭バイト(マジックバイト/ファイル署名)が宣言した形式と一致するかを中身で確かめ、検証は名前をデコードした後に行います。拡張子の素朴なチェックは二重拡張子・ヌルバイト・大文字小文字の違いで回避されるためです。
ただし署名の一致は「先頭が正しい形式に見える」ことしか保証しません。OWASP も署名検査を単体で使わず他の層と組み合わせるよう勧めています。確認方法ごとに、防げることと防げないことを分けて持っておきます。
| 確認方法 | 防げること | 防げないこと |
|---|---|---|
| 拡張子チェック(許可リスト) | 想定外の形式の受け入れ | 二重拡張子・中身が別物のファイル |
| Content-Type ヘッダー | ほぼ無い(送信側の自己申告) | 偽装が容易で単独では当てにならない |
| マジックバイト(署名)検査 | 先頭が宣言した形式と違うファイル | 署名の後ろに別の内容を抱えたファイル |
| 画像の再エンコード | 埋め込み・余分なメタデータ | すべての悪性が消える保証はない |
表のとおり、どれか一つで足りる方法はありません。許可リスト・中身の検証・保存先の設計・配信の仕方を重ねて初めて、1か所の抜けが致命傷になりません。
保存先の設計:Webルート外・実行権限なし・名前はサーバーが決める
検証を抜けたファイルでも、危険の大きさは置き場所しだいです。OWASP は保存先を、別ホスト、次にWebルートの外(管理者だけがアクセスできる場所)の順で勧めています。ASVS 12.4.1 も信頼できない入力元のファイルをWebルート外に限定した権限で保存すること、12.5.2 は直接要求してもHTML/JavaScriptとして実行されないことを求めています。
守りは次の三つです。
・Webルートの外に置く。公開ディレクトリ直下に保存すると、URLを直接叩かれて実行・解釈される余地が残る
・保存ディレクトリで実行ハンドラを無効にする。OWASP はアップロードディレクトリでのディレクトリ単位の設定上書きを無効にするよう勧めている(攻撃者が設定ファイルを置く手を塞ぐ)
・保存名はサーバーが決める。UUIDなどのランダムな名前を生成し、利用者が送った元の名前をそのままパスに使わない。元の名前を保持する必要があれば、英数字・ハイフン・ピリオドなどの許可した文字だけに絞る
ファイル名をそのまま信じると、../ を含む名前で想定外の場所に書き込まれたり、既存ファイルを上書きされたりします。ASVS 12.3.1 も、利用者が送ったファイル名のメタデータをファイルシステムの操作に直接使わないことを求めています。この「名前を信用しない」話は、次回(第24回)のパストラバーサルと地続きです。
画像の再エンコードとサイズ・件数の制限
画像なら安全、とは言えません。画像ファイルには余分なメタデータや別の内容を抱かせられるため、信頼できる画像ライブラリで一度デコードし、許可する形式に再エンコードして不要なメタデータを落とします。OWASP はこの再エンコードを勧める一方で、それですべての悪性が除去される保証はないとも注意しています。
再エンコードに使う画像処理ライブラリ自体にも脆弱性は出ます。依存の脆弱性をどれから直すかは「npm audit の結果はどれから直す?依存関係の脆弱性を到達性・EPSS・KEVで仕分ける実践手順」の考え方が使えます。
DoSへの備えも設計の一部です。ASVS 12.1.1 はストレージを埋めたりサービス拒否を招くほど大きいファイルを受け付けないこと、12.1.3 は利用者ごとの総容量と件数の上限、12.1.2 は圧縮ファイル(zip・gz・docx など)を展開後のサイズと件数に照らして確かめることを求めています。アップロードの頻度にもレート制限を置き、必要ならアンチウイルスやサンドボックスに通します(ASVS 12.4.2)。
Content-Type・Content-DispositionとSVGの扱い
最後の段は配信です。保存したファイルをどう返すかで、ブラウザの中での危険が変わります。
WHAT TO SERVE通す/止める
配信は実行させず、別経由で、名前はサーバー
uploads/(Webルート外・静的配信のみ)
- 画像再エンコード済み画像 添付として配信許可リストの形式
- 実行.php など実行ハンドラ 実行させない(403)拡張子を偽装した投稿
- SVGSVGのインライン表示 別オリジン or 無害化scriptを含むXML
- 名前元のパスで配置 サーバーが名前を決める../ を含む名前
公開して返すファイルは、レスポンスで Content-Disposition: attachment を付けて添付(ダウンロード)扱いにし、X-Content-Type-Options: nosniff でブラウザによる種類の推測を止めます。ASVS 12.5.2 が言うとおり、直接要求されてもHTML/JavaScriptとして実行されないことが目標です。
とくに注意が要るのがSVGです。SVGはXMLで、内部に script を書けるため、同一オリジンでインライン表示すると、閲覧者のブラウザでスクリプトが動くXSSになりえます。別オリジンから添付配信するか、サーバー側で無害化してから出します。この「どこで実行されるか」という視点は「XSS対策は出力時のエスケープとCSP|入力で消さず出す場所で変換し、nonceとTrusted Typesで止める」と同じ考え方です。
クラウドストレージへ直接アップロードさせる場合は、署名付きURLの有効期限・権限・保存先のパスをサーバーが固定し、利用者が送るキー名をそのまま使わないようにします(公開設定そのものは第37回で扱います)。
自分の環境で確かめる
保存先が設計どおりかは、読み取りだけのコマンドで確かめられます。これは判定ではなく構成の確認で、この結果だけでは安全とは言えません。
# 1) 拡張子ではなく中身でファイルの種類を見る(宣言と中身が食い違う例を洗い出す)
file --mime-type uploads/*
# 2) 保存先に実行権限の付いたファイルが無いか(陰性なら何も出ない)
find uploads -type f -perm -u+x
# 3) Nginx の設定にアップロード配下の拒否ルールが入っているか
grep -nE 'uploads|return 403' nginx.conf
1つめは file が中身を読んで種類を返すので、photo.jpg の中身が実はPNGやテキストといった食い違いが見えます。2つめは、保存ディレクトリに実行権限の付いたファイルが残っていないかの一覧です。3つめは配信側で実行を止める設定の有無を確かめます。
結果の読み方には限界があります。陽性(食い違いや実行権限が見つかった)でも、Webルート外で実行ハンドラも無効なら直ちに実行はされません。逆に陰性(何も出ない)でも、設定の上書きが効くディレクトリや別の配信経路までは見ていないので、安全の証明にはなりません。最終的な判断は、中身の検証・保存先・配信の設定が設計どおり揃っているかで下します。設定を変える手順は各サーバーの公式ドキュメントに従ってください。
参照ソース
- OWASP File Upload Cheat Sheet(拡張子の許可リスト・Content-Typeの不信・署名検査・ファイル名・保存先・再エンコード)
- OWASP ASVS v4.0.3 V12 Files and Resources(12.1 サイズ・件数・解凍/12.3 ファイル名/12.4 保存先・アンチウイルス/12.5 実行の防止)





