PHPでファイルをアップロードする方法と脆弱性対策

PHPでのファイルアップロードの実装方法から気を付けることまでを解説します。

ファイルアップロードの仕組み

  1. ブラウザに表示されたアップロードフォームよりファイルを選択してサーバーへ送信します。
  2. 送信されてきたファイルはファイルの受信が終わるまで、作業ディレクトリへ一時的に保存されます。
  3. 作業ディレクトリはphp.iniの項目「upload_tmp_dir」で設定できます。 「upload_tmp_dir」が未設定の場合はOS標準のディレクトリ(通常は/tmp/)へ保存されます。
  4. move_uploaded_file関数を使用して、一時ディレクトリからデータフォルダへコピーします。
  5. 一時保存されたファイルはアップロードが終わりphpの処理が実行を終えると削除されます。

サンプルコード

先ずはHTML側のサンプルコードを書いてみます。

ファイルを選択して、送信ボタンで送信するという単純な処理です。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PHPファイルアップロードサンプル</title>
</head>
<body>
<form action="./up.php" method="POST" enctype="multipart/form-data">
	<input type="file" name="file_upload_test">
	<input type="submit" value="送信">
</form>
</body>
</html>

method="POST"はデータをサーバーに送信する場合に必要で、enctype="multipart/form-data"は複数の種類のデータを送信する場合に必要になります。

ファイルをアップロードする場合はmethod="POST"とenctype="multipart/form-data"は必須です。

formの中にはファイルを選択するinputとファイル送信ボタンを配置しました。

ファイル送信する場合はinputをtype="file"にして、phpでファイルを受け取るときに使用する名前をnameに設定します。ここでは「file_upload_test」としました。

PHPのコードは以下のようになります。

ファイルを受信して一時ファイルを指定のデータディレクトリに保存するコードです。保存名はアップロードされたファイル名をそのまま使用しています。

<?php
// 保存先のディレクトリ + アップロードしたファイル名
$upload_file_path = "./" . $_FILES["file_upload_test"]["name"];
// アップロードが有効ならデータディレクトリにコピー
if(move_uploaded_file($_FILES["file_upload_test"]["tmp_name"],$upload_file_path)){
	print("アップロード完了");
} else {
	print("アップロード失敗");
}

受け取ったファイルの情報を扱うにはスーパーグルーバル変数の$_FILESを使用します。

HTMLフォームで付けたinputのnameを使用してアップロードされたファイル情報を取得します。

$_FILES["file_upload_test"]

このコードではファイル情報が連想配列で取得できます。

項目意味
nameオリジナルのファイル名
typeファイルのMIMEタイプ(データの形式)
tmp_name作業ディレクトリでの一時ファイルパス
errorエラー内容
sizeファイルサイズ

例えば画像ファイルの$_FILES情報を出力すると以下のようになっています。

array(5) {
  ["name"]=>
  string(24) "オリジナルのファイル名.jpg"
  ["type"]=>
  string(10) "image/jpeg"
  ["tmp_name"]=>
  string(14) "/tmp/phpy6M5Un"
  ["error"]=>
  int(0)
  ["size"]=>
  int(264639)
}

「オリジナルのファイル名.jpg」を選択し、送信ボタンを押すと、同じファイル名でサーバーに保存されます。

脆弱性対策

上記のphpのサンプルコードには、任意のファイルを上書きできる脆弱性があります。

「ディレクトリトラバーサル攻撃」と言われ、アプリ側で受け取ったファイル名などを未検査で使用することで、重要なファイルを閲覧、改ざん、削除されてしまうものです。

今のサンプルコードでは元ファイル名をそのまま保存名にしているので、もしかしたら元ファイル名に「../../../../../www/index.php」なんて付けられて、phpファイルが改ざんされてしまうかもしれません。

また、ファイル名が被る可能性があるのでこれらを対策します。

対策後のコード

1.専用の保存ディレクトリ「data」を用意し、アップロード時点の年月日時分秒、ランダムな0~10000の値を使用してファイル名が重複しないようにします。また元ファイル名は使用しません。

// 保存先のディレクトリ + 年月日時分秒 + ランダムな0~10000の値 + アップロードしたファイル名(対策済)
$upload_file_path = "./data/" . date("YmdHis") . rand(0,10000);

2.ファイル名はbasename関数を使用することで$_FILES["file_upload_test"]["name"]の値が予期しない重要なファイルへのパス(例:「../../web/index.html」等) になっていたとしても、「index.html」となります。
$upload_file_pathの値は「./data/index.html」になる為回避可能です。
こっちはファイル名の重複チェックと組み合わせると良いです。

// 保存先のディレクトリ + アップロードしたファイル名(対策済)
$upload_file_path = "./data/" . basename($_FILES["file_upload_test"]["name"]);
// ファイル名が同じものが存在せず、アップロードが有効ならデータディレクトリにコピー
if(!file_exists($upload_file_path) && move_uploaded_file($_FILES["file_upload_test"]["tmp_name"],$upload_file_path)){
	print("アップロード完了");
} else {
	print("アップロード失敗");
}

3.基本的にはファイル名は固定か連番若しくは内部で独自に生成するのが良いです。

外部からのパラメータを使用しないことが一番安全だと思います。

// 保存先のディレクトリ + アップロードしたファイル名(対策済)
$upload_file_path = "./data/upload.dat";

サンプルコードの注意点

サンプルコードにはファイル容量や種類の制限がありませんので、実際のアプリに組み込む場合はこれらについても考える必要があります。

何でも受け付けるとバックドアを仕込まれる可能性もありますからね。

ファイル容量も制限を設けないと、すぐにディスク使用量が一杯になるかもしれません。