ファイルアップロードの危険 — 画像のつもりが、実行ファイルを置かれる

ファイルアップロードは「画像を1枚受け取るつもり」が「実行できるファイルを置かれる場所」になりやすい穴。攻撃者は画像のふりをした「サーバーを操作するプログラム(Webシェル)」を送り込む。2025年の SAP の穴(CVE-2025-31324、CVSS 10.0)では認証なしでアップロードでき、サーバーを乗っ取られた。拡張子は送る側が自由に付けられるので「拡張子チェック」はすり抜けられる。守りは①中身で種類を確認②実行されない専用ストレージに置く③名前とサイズを決め直す。頼み方と確かめ方。

動画で見る

結論から

ファイルアップロードの機能は、「画像を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 インジェクション)など、種類ごとの注意はありますが、原則(中身確認・実行されない場所・名前付け直し)は共通です。

この穴が塞がれば、アップロードは安全ですか。

「実行ファイルを置かれる」ことは防げます。「アップロードしたファイルを他人が見られる」(認可)、「巨大ファイルでサーバーを止める」(サイズ上限)は、それぞれ別に確認してください。