レガシーPHPリファクタ 6:レガシーな画像アップロード処理をリファクタリング!安全性の高い実装へ改善する3つのポイント

昔作られたWebシステムや、初心者の学習用としてコードの単純さを優先した実装では、画像アップロード処理にセキュリティ上の脆弱性や思わぬ不具合が潜んでいることがよくあります。

今回は、古い実装でよく見かけるレガシーなPHPコードを例に、実務でも安心して使えるレベルへとリファクタリングする4つの改善ポイントを解説します。


改善1:画像ディレクトリパスの「末尾スラッシュ」を排除してパス結合を明瞭化する

パスの末尾にスラッシュを含めるかどうかのルールが曖昧だと、コード全体でパス結合時の表記揺れや二重スラッシュ(//)が発生しやすくなります。

修正前

$imageDir = './images/';
...
$targetPath = $imageDir . $fileName;

修正後

// ※結合時に二重スラッシュ(//)になるのを防ぐため末尾の '/' は除外
// (使用時は $imageDir . '/' . $fileName と指定)
$imageDir = './images';
...
$targetPath = $imageDir . '/' . $fileName;

解説

ディレクトリ変数は「末尾スラッシュなし」で標準化し、パスを結合する側で常に明示的に '/' を挟むように統一します。これにより、二重スラッシュの発生を防ぎ、変数がどのような形式のパスを保持しているのかが一目で理解できるようになります。


改善2:画像ディレクトリが存在しない場合は自動作成せずエラー終了させる

動的な mkdir() による自動作成は、書き込み権限の管理不足や予期せぬディレクトリ階層の生成につながる恐れがあります。

修正前

if (!is_dir($imageDir)) {
    mkdir($imageDir);
}

修正後

// 保存先ディレクトリの存在確認(存在しない場合は環境・設定エラーとして終了)
if (!is_dir($imageDir)) {
    http_response_code(500);
    echo '画像保存ディレクトリが存在しません。環境設定を確認してください。';
    exit;
}

解説

本番環境においては、保存用ディレクトリは適切な所有者・パーミッションを設定した上で事前に用意しておくのが基本です。コード内で自動作成するのではなく、環境不備として 500 Internal Server Error を返して安全に処理を中断させることで、権限トラブルや不正なパスでのディレクトリ作成を防ぎます。


改善3:is_uploaded_file + copy をやめ、move_uploaded_file と適切なエラーチェックを使う

一時ディレクトリにアップロードされたファイルを扱う際、古い実装では copy() が使われることがありますが、これには移動失敗時やセキュリティ面の考慮が欠けています。

修正前

$imageFile = '';
if (isset($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
    $imageFile = $imageDir . $_FILES['image']['name'];
    copy($_FILES['image']['tmp_name'], $imageFile);
}

修正後

$imageFile = '';
if (isset($_FILES['image']['tmp_name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
    $targetPath = $imageDir . '/' . basename($_FILES['image']['name']);
    if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) {
        $imageFile = $targetPath;
    }
}

解説

  1. UPLOAD_ERR_OK によるエラー確認 ファイルサイズ超過や転送中断などのエラーメッセージを正しく受け取るため、$_FILES['image']['error'] の確認を行います。
  2. move_uploaded_file() の採用 HTTP POST経由でアップロードされたファイルかどうかを判定しつつ安全に移動させるため、単なる copy() ではなく専用の move_uploaded_file() を使用します。処理後に一時ファイルが自動消去され、サーバーのリソース管理も安全に行われます。
  3. basename() によるパス・トラバーサル対策 元ファイル名に含まれる ../ などの悪意あるパス操作を防ぐため、必ず basename() を通します。※ファイル名に関して、改善4 へ続きます

改善4:ファイル名の重複衝突とセキュリティリスクを防ぐ

入門書や簡易的なサンプルコードでは、コードを単純にするためにユーザーが送信したファイル名をそのまま利用することがあります。しかし、本番環境では同じファイル名の上書き衝突や、ファイル名に特殊文字が含まれる問題が発生します。

修正前

$targetPath = $imageDir . '/' . basename($_FILES['image']['name']);

修正後

// 拡張子の取得(小文字化)
$extension = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));

// 一意なファイル名の生成(例: 20261020_65f1a2b3c4d5.jpg)
$fileName = date('Ymd_') . bin2hex(random_bytes(8)) . ($extension !== '' ? '.' . $extension : '');

$targetPath = $imageDir . '/' . $fileName;

解説

  1. ランダムな一意(ユニーク)キーの付与 date('Ymd_') と暗号学的に安全なランダムバイト列(random_bytes(8))を組み合わせることで、同日・同秒の同時アップロードであっても衝突しないファイル名を生成します。
  2. 拡張子の標準化 pathinfo() で拡張子のみを抽出し、strtolower() で小文字に変換します。これにより .JPG や .Jpg などの表記ゆれを扱いやすく統一します。

まとめ:完成したリファクタリング後コード

最後に、これら4つの改善を統合したコード全体の例です。

$imageDir = './images';
$imageFile = '';

// 保存先ディレクトリの存在確認
if (!is_dir($imageDir)) {
    http_response_code(500);
    echo '画像保存ディレクトリが存在しません。環境設定を確認してください。';
    exit;
}

if (isset($_FILES['image']['tmp_name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
    // 拡張子の取得(小文字化)
    $extension = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));

    // 一意なファイル名の生成
    $fileName = date('Ymd_') . bin2hex(random_bytes(8)) . ($extension !== '' ? '.' . $extension : '');

    $targetPath = $imageDir . '/' . $fileName;

    // ファイル移動処理
    if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) {
        $imageFile = $targetPath;
    }
}

入門用のシンプルな処理から一歩進み、不具合の起きにくい安全なコードに整えることで、保守性や堅牢性が格段に向上します。古いPHPコードを扱う際は、ぜひこれらの観点で見直してみてください。

参考PR

refactor(chapter2): 画像アップロード処理の安全性向上とディレクトリ定義規約の統一 by ninton · Pull Request #711 · ninton/smartybook
概要02_01_b.php(設定ファイル)におけるディレクトリ定義の命名規約を整理し、02_05.php(登録確認画面)での画像保存処理を現代的なファイル操作手法およびエラーハンドリング(500 Internal Server Error)...
refactor(chapter2): 画像アップロード時のファイル名衝突・上書き防止(一意なファイル名の自動生成) by ninton · Pull Request #712 · ninton/smartybook
概要02_05.php(登録確認画面)において、アップロードされた画像の元ファイル名をそのまま使用していた処理を改修し、一意なファイル名(タイムスタンプ+ランダムハッシュ+拡張子)を自動生成して保存する構造へ変更しました。変更内容ファイル名...
レガシーPHPリファクタ 6:レガシーな画像アップロード処理をリファクタリング!安全性の高い実装へ改善する3つのポイント
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.
タイトルとURLをコピーしました