結論から
ファイルアップロードの機能は、「画像を1枚受け取るつもり」が「実行できるファイルを置かれる場所」になりやすい穴です。プロフィール画像、証明書 PDF、CSV。ユーザーがファイルを送れる場所はどこでも、攻撃者は画像のふりをした「サーバーを操作するプログラム(Webシェル)」を送り込もうとします。2025年、SAP の製品に見つかったこの種の穴(CVE-2025-31324、深刻度は最高の10.0)では、認証なしでファイルをアップロードでき、サーバーを乗っ取られました。「拡張子だけ見て許可する」チェックは、簡単にすり抜けられます。
この記事で伝えたいのは、この一点だけです。アップロードされたファイルは「危険物」として扱う。①中身で種類を確認する(拡張子だけを信じない)、②アップロード先を「実行できない場所」にする(自分のサーバーに置かず、Supabase Storage・S3・Cloudflare R2 のような専用ストレージに)、③名前とサイズを自分で決め直す。 AI は「アップロードできる」までは作りますが、この3つは頼まれない限り抜けます。
- 何が起きるか:
profile.jpgのふりをしたshell.php(サーバーで動くプログラム)をアップロードされる。それが実行できる場所に置かれると、攻撃者がそのファイルの URL を開くだけで、サーバーのファイルを読んだり、DB に接続したりできる - なぜ:ファイル名の拡張子(
.jpg)は、送る側が自由に付けられる。中身が画像かどうかは、拡張子を見ても分からない - 直す:中身の先頭バイト(マジックナンバー)で種類を確認。専用ストレージ(実行されない)に保存。ファイル名はサーバー側でランダムに付け直す。サイズ上限。画像は再エンコード
- 確かめる:拡張子を
.jpgにしたテキストファイルをアップロードしてみる。通ったら、中身を見ていない。アップロードしたファイルの URL を開いて、ソースが実行されずに「ダウンロード」になるか
動画版(約2分半)は本文の上に埋め込んであります。
① 何が起きるのか:画像のふりをした、プログラム
例として、AI で作った予約アプリに「プロフィール画像をアップロード」があるとします。佐藤さんが sato.jpg を選んで送ると、サーバーがそれを保存し、画面に表示します。正常です。
攻撃者は、sato.jpg の代わりに、こういうファイルを送ります。ファイル名は avatar.php(または .jsp、.aspx など、サーバーの種類による)。中身は、画像ではなく「このサーバーで、送られてきた命令を実行する」プログラムです。
もし、あなたのサーバーがこのファイルを「アップロードされた場所にそのまま置き」、かつ「その場所のファイルをプログラムとして実行できる」設定なら、攻撃者は https://あなたのアプリ/uploads/avatar.php?cmd=... を開くだけで、あなたのサーバーの中で好きな命令を動かせます。ファイルを読む、環境変数(DB パスワード・API キー)を盗む、DB に接続する。これを「Webシェル」と呼びます。
「うちは拡張子をチェックしているから大丈夫」と思うかもしれません。しかし、拡張子チェックはよくすり抜けられます。avatar.php.jpg、avatar.jpg(中身は PHP で、サーバーの設定次第で実行される)、大文字小文字(.PHP)、画像として正しいヘッダを付けた上で末尾にコードを埋めた「ポリグロット」ファイル。2026年に Magento で見つかった穴(PolyShell)は、まさに「画像に偽装したコードで検証をすり抜ける」ものでした。
2025年4月に SAP NetWeaver で見つかった CVE-2025-31324 は、認証すら不要でファイルをアップロードでき、JSP のWebシェルを置かれてサーバーを乗っ取られました。深刻度は 10.0(最高)。プロが作った製品でも起きる穴です。
② なぜ起きるのか:拡張子は、送る側が自由に付けられる
原因は、ファイル名も拡張子も、送る側が自由に決められることです。shell.php を photo.jpg という名前で送るのは、攻撃者にとって一瞬です。だから「名前が .jpg だから画像だ」という判断は、成り立ちません。
守り方は、名前ではなく中身と置き場所です。3つあります。
① 中身で種類を確認する。 ファイルの先頭の数バイト(マジックナンバー)を見ると、本当に PNG か JPEG かが分かります。「拡張子が .jpg で、かつ中身の先頭が JPEG のもの」だけを通す。さらに、画像ならサーバー側でもう一度エンコードし直す(リサイズ・変換)と、埋め込まれたコードは落ちます。
② 実行できない場所に置く。 これが一番効きます。アップロードされたファイルを、あなたのアプリが動いているサーバーの中ではなく、専用のストレージ(Supabase Storage・Amazon S3・Cloudflare R2)に保存する。これらは「ファイルを配るだけ」で「プログラムとして実行しない」ので、.php を置かれても、ただのファイルとしてダウンロードされるだけです。AI で作るアプリの多くはこれらを使えるので、ここを押さえるのが現実的です。
③ 名前とサイズを、サーバーが決め直す。 ユーザーが付けた名前は捨て、サーバー側でランダムな名前(a8f3c2e9.jpg)を付ける。拡張子も、確認した中身に合わせてサーバーが付ける。サイズの上限も決める(巨大ファイルでサーバーを一杯にする攻撃を防ぐ)。
AI はどう作るか。 「画像アップロードを付けて」で作られるのは、①受け取る、②保存する、③表示する、まで。中身の確認、置き場所が実行可能かどうか、名前の付け直しは、頼まれない限り入りません。特に、AI が「簡単だから」とアプリと同じサーバーの public/uploads に保存するコードを書くと、②の穴が開きます。
③ どう直すのか:AIに「中身・置き場所・名前」を頼む
以下をそのまま AI ツールに貼ってください。
「このプロジェクトのファイルアップロード機能について、①アップロードされたファイルの種類を、拡張子だけでなく中身(マジックナンバー / MIME の実チェック)で検証しているか、②ファイルの保存先が、アプリの実行環境の中(public/ など)か、実行されない専用ストレージ(Supabase Storage / S3 / R2)か、③保存するファイル名を、ユーザー入力のままではなくサーバー側で付け直しているか、④サイズ上限があるか、を確認してください。①が拡張子のみなら中身チェックを追加し、画像は保存前に再エンコードしてください。②がアプリ内なら専用ストレージに変更してください。③がユーザー名のままならランダムな名前に変更してください。変更前後を一覧で示してください。」
ポイントは、②の「実行される場所か」。これが最も被害の大きい穴で、専用ストレージに移すだけでほぼ塞がります。
④ 直ったかを確かめる:.jpg にしたテキストを送る
- テキストファイルを作り、名前を `test.jpg` に変えてアップロードする。エラーにならず通ったら、中身を見ていない ✗。「画像ではありません」で弾かれたら ◯
- アップロードした画像の URL をブラウザで直接開く。画像が表示されるのは正常。もし
.phpなどを上げられて、それが実行されたら(エラーページや命令の結果が出たら)✗。ダウンロードになるか、ただのテキストとして見えるなら、実行はされていない - アップロード先の URL のドメインを見る。
あなたのアプリ.com/uploads/...ではなく、...supabase.co/storage/...や...r2.dev/...のような専用ストレージなら ◯ - 保存されたファイル名が、
sato.jpgのまま(ユーザーが付けた名前)か、ランダムな名前か。ランダムなら ◯ - 分からなければ、AIに「拡張子を偽装したファイル(中身がテキストの
evil.jpg)がアップロードで弾かれることを確認するテストを書いて」
自分で直せないとき:エンジニアに送る一文
「ファイルアップロードで、中身の検証(マジックナンバー)、実行されないストレージへの保存、ファイル名の付け直し、サイズ上限、が入っているか確認してください。特に、アプリと同じサーバーの公開ディレクトリに保存していないかを見てほしいです。」
よくある質問
Supabase Storage を使っています。大丈夫ですか。
保存先としては、実行されないので②はクリアです。ただし、①中身のチェックと③名前の付け直しは別途必要です(誰でも読めるバケットになっていないか、という「認可」の話も。→ 認証と認可の記事)。
画像しか受け付けないようにしています。
「拡張子で .jpg .png だけ許可」なら、すり抜けられます。中身の先頭バイトを見る、保存前に再エンコードする、の2つを足してください。
PDF や CSV のアップロードも同じですか。
同じです。PDF に埋め込まれたスクリプト、CSV を Excel で開いたときに実行される数式(CSV インジェクション)など、種類ごとの注意はありますが、原則(中身確認・実行されない場所・名前付け直し)は共通です。
この穴が塞がれば、アップロードは安全ですか。
「実行ファイルを置かれる」ことは防げます。「アップロードしたファイルを他人が見られる」(認可)、「巨大ファイルでサーバーを止める」(サイズ上限)は、それぞれ別に確認してください。


