やろうとしたこと
やろうとしたことは超シンプルです。- 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」は絶対パスを取得するようです。- SplFileInfo::getPathname
- ファイルへのパスを取得する
- SplFileInfo::getRealPath
- ファイルへの絶対パスを取得する
$file = $request->files->get('file');
のところでnullが取得されて特に被害は発生しないのですが、サイズ上限オーバーの場合は中途半端にファイルが存在する感じになっていそうです。 今回は事例紹介まで。