【入門編】 安全なファイルアップロード機能の実装とディレクトリトラバーサル対策 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

こんにちは!Webアプリケーション開発の世界へようこそ。
新人のIT担当者さんや、「セキュリティって何だか難しそう……」と初めて向き合う開発者さんに向けて、今回は実務で絶対に避けて通れない「安全なファイルアップロード機能の実装」についてお話ししていきますね。

プロフィール画像や添付文書のアップロード機能は、どんなWebサイトにもある便利な機能です。でも実はここ、サイバー攻撃者にとっては「一番侵入しやすい勝手口」になりやすい危険な場所でもあるんです。

今回は、家の防犯に例えながら、攻撃の仕組みとしっかりとした対策を一緒に優しく学んでいきましょう!

—

1. ファイルアップロード機能は「自宅の郵便受け」と同じ?

想像してみてください。あなたの家には、外から手紙を受け取るための「郵便受け(ポスト)」がありますよね。便利ですが、もしここに誰でも大きすぎる荷物を無理やり突っ込めたり、泥棒が爆発物や怪しい薬物をポイッと投げ入れたりできたら……想像するだけでゾッとしますよね。

Webサイトにおける「ファイルアップロード機能」も、まさにこれと同じです。
ユーザー(お客さん)が自由にファイルを放り込める場所を作ってしまうと、悪意のある攻撃者が「動くプログラム(不正なコード)」を送り込み、それをサーバー上で実行させてしまうという恐ろしい事態が起きてしまいます。これが、今回防ぎたい任意コード実行(RCE)やディレクトリトラバーサルと呼ばれる攻撃の正体です。

—

2. なぜ危ない? 攻撃者が狙う3つの手口

攻撃者は、あなたの作ったアップロード機能に対して、主に次のような手口で攻めてきます。一歩ずつその仕組みを見ていきましょう。

① 「実はプログラムでした」詐欺(拡張子偽装とMIMEタイプのすり抜け)

人間は見た目で判断しますが、コンピューターは素直です。例えば、拡張子が .php や .jsp なら「これはプログラムだから実行しよう」と動きますが、これを .jpg という画像ファイルのふりをさせてアップロードさせます。
中身はバリバリの攻撃プログラムなのに、名前だけ「cute-cat.jpg.php」のようにしたり、サーバー側で中身の確認を怠っていたりすると、サーバーはそれをプログラムとして実行してしまいます。これが「家の鍵穴に、泥棒が合鍵を差し込む瞬間」です。

② 「勝手に別の部屋へ移動します」詐欺(ディレクトリトラバーサル)

ファイル名として ../../etc/passwd や ../../index.php のような特殊な文字列(相対パス)を指定してアップロードする手口です。
もし開発者が「送られてきたファイル名をそのままサーバーの保存に使おう」と考えていると、サーバーの意図しない階層(本来は触ってほしくないシステムの大切なフォルダ)にファイルが保存・上書きされてしまい、サイトを乗っ取られてしまいます。

—

3. 鉄壁の守りを作る! 4つの必須対策

「うわ、なんだか怖くなってきた……」と思いましたか? 大丈夫です!
しっかりとした防犯対策(バリケード)を順番に作っていけば、攻撃者を完全にシャットアウトできます。実務で必ず実装してほしい4つのポイントを確認していきましょう。

対策1:ファイル名はサーバー側で完全に再生成する(ファイル名サニタイズ)

ユーザーがアップロードしてきたファイル名を、そのまま信じて使ってはいけません。「どんな名前であれ、サーバー側で強制的に書き換える」のが鉄則です。
例えば、PHPを使う場合は以下のように、ランダムな文字列(UUIDやハッシュ値)をファイル名に割り当て、元の拡張子は安全なホワイトリスト方式で厳選します。

<?php
// アップロードされたファイルの情報
$file = $_FILES['user_file'];

// 1. 許可する拡張子のホワイトリスト(これ以外は絶対に通さない!)
$allowed_extensions = ['jpg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif'];

// 2. 拡張子の取得とチェック
$file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!array_key_exists($file_extension, $allowed_extensions)) {
    die('エラー: このファイル形式はアップロードできません。');
}

// 3. MIMEタイプ(ファイルの実体)の厳密な検証
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file['tmp_name']);
if ($mime_type !== $allowed_extensions[$file_extension]) {
    die('エラー: ファイルの中身と拡張子が一致しません。');
}

// 4. ファイル名を完全にランダムな文字列に再生成(ディレクトリトラバーサル対策)
// 例: a1b2c3d4e5f6...jpg のような安全な名前にする
$new_filename = bin2hex(random_bytes(16)) . '.' . $file_extension;

// 保存先ディレクトリ(Webから直接実行できない場所、または安全な公開フォルダ)
$upload_dir = '/var/www/uploads/';
$destination = $upload_dir . $new_filename;

// 5. ファイルの移動
if (move_uploaded_file($file['tmp_name'], $destination)) {
    echo 'ファイルのアップロードが完了しました!';
} else {
    echo 'エラーが発生しました。';
}

対策2:保存先をWebの公開ルートから隔離する(保存先分離)

アップロードされたファイルを、誰でもブラウザから直接URLでアクセスできる場所(例: https://example.com/uploads/ など)にそのまま置いていませんか?
もし .php などのプログラムファイルが万が一アップロードされてしまった場合、そのURLにアクセスするだけでプログラムが実行されてしまいます。

【ベストプラクティス】

  • アップロードされたファイルは、Webサーバーのドキュメントルート(公開領域)の外側に保存する。
  • もし画像などをユーザーに見せる必要がある場合は、専用のプログラム(画像ローダー)を経由して、権限チェックを行った上で安全に出力する。

対策3:アップロードディレクトリでの「スクリプト実行禁止」設定

インフラ側の設定も非常に重要です。もしファイルを公開領域に保存せざるを得ない場合でも、ApacheやNginxなどのWebサーバーの設定で、そのフォルダ内では「プログラムを実行させない」という制限をかけます。

Apacheの場合は、アップロードフォルダ内に .htaccess という設定ファイルを置き、以下のように記述してガードします。

# このフォルダ内ではPHPなどのスクリプトの実行を一切禁止する
<FilesMatch "\.(php|phtml|php3|php4|php5|pl|py|jsp|asp|sh)$">
    Order allow,deny
    Deny from all
</FilesMatch>

対策4:ファイルサイズの制限(DDoS・容量圧迫対策)

攻撃者は、何ギガバイトもある巨大なファイルを送りつけてサーバーをパンクさせようとすることがあります(これを防ぐのもセキュリティの大切な仕事です)。
HTMLフォーム側とサーバー側の両方で、ファイルの最大サイズを厳しく制限しましょう。

HTML側での制限例:

<!-- フォーム送信時に最大ファイルサイズ(例: 2MB)を指示する -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
    <!-- 隠しフィールドで最大バイト数を指定(約2MB) -->
    <input type="hidden" name="MAX_FILE_SIZE" value="2097152">
    <label>画像をアップロード(2MBまで):</label>
    <input type="file" name="user_file">
    <button type="submit">送信する</button>
</form>

—

4. まとめ:安全な実装は「疑うこと」から始まる

いかがでしたでしょうか?
安全なファイルアップロード機能を作るための基本をまとめると、以下のようになります。

1. ユーザーが入力したファイル名は信用せず、完全に捨ててランダムな名前に変える。
2. 拡張子とMIMEタイプ(中身)の両方を二重でチェックする。
3. 保存先はWebから直接実行できない場所にするか、スクリプト実行を禁止する。

セキュリティの世界では、「ユーザーが送ってくるデータはすべて悪意あるものである」と仮定して疑うことから設計が始まります。最初は少し面倒に感じるかもしれませんが、一度このパターンを覚えてしまえば、どんなシステム開発でも応用が利く強力な武器になります。

一歩ずつ、確実に安全なコードを書けるエンジニアを目指して一緒にがんばっていきましょう!

コメント

タイトルとURLをコピーしました