PHPのgetRealPathでファイルパスの代わりにプロジェクトルートが取れてしまった話

スズキです。 Webアプリの画面からファイル添付してサーバに送ることってありますよね。今回は、サーバ側のPHPプログラムでリクエストから添付ファイルを取り出そうとしたら、思ってたのと全然違う挙動になってしまった話です。

やろうとしたこと

やろうとしたことは超シンプルです。
  • HTTPのmultipart/form-dataで送られてきたファイルをリネームして保存する
  • ファイルが無ければエラーにする

サンプルコード(修正前)

PHP+Symfonyのコードです。例外処理などはごっそり省略。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getRealPath();
    
    // ③ファイル存在チェック
    if (!$tmpFilePath) {
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}

起きたこと

②でファイルパスを取得したとき、通常なら、 // $tmpFilePath : ‘/tmp/phpucYAwE’ といった一時ファイルのパスが取得できるのですが、PHPのアップロードサイズ上限値(php.iniで設定する値)を超えたファイルを送ってみると、
// $tmpFilePath : '/opt/myWebApp/public'
といったように、なぜかWebアプリのルートディレクトリパスが取得されてしまいました。そして、③のファイル存在チェックでもエラー判定できず、そのまま④でアプリのルートディレクトリがリネームされてアプリが動かなくなる事態に…

対処

②でのファイルパス取得にて「getRealPath」を「getPathname」に変えたところ、アップロードサイズ上限を超えたファイルの場合に、期待通りに空文字が取得できるようになりました。
ファイルサイズが大きくない場合は、「getRealPath」と「getPathname」は同じパスを返してくれたので、問題なさそうです。
// $tmpFilePath : '' ←ファイルサイズ上限オーバーのときは空文字が返ってくる(期待通り)

サンプルコード(修正後)

ディレクトリパスが取得されてしまうと悲惨なので、②のパス取得部分だけでなく、安全のため③のファイル存在チェックの条件も増やしています。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getPathname(); // getRealPath ではなく getPathname を使用
    
    // ③ファイル存在チェック
    if (!$tmpFilePath || is_dir($tmpFilePath)) { // パスがディレクトリだった場合もエラーにしておく
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}
 

原因推測

「getPathname」と「getRealPath」はどちらも似た説明になっていますが、「getRealPath」は絶対パスを取得するようです。 「getRealPath」が絶対パスに変換する処理の中で、空文字のパスがプロジェクトルートと解釈されたのかな…と推測しています。 リクエストに添付ファイル自体が無ければ、①の $file = $request->files->get('file'); のところでnullが取得されて特に被害は発生しないのですが、サイズ上限オーバーの場合は中途半端にファイルが存在する感じになっていそうです。 今回は事例紹介まで。