画像アップローダーの実装。(サーバーサイド)
CMSは必要ない仕様で画像アップローダーだけ実装したかったので、JavaScirptとPHPで実装した方法のメモ。(サーバーサイドの処理。)
※ クライアントサイドの処理は前の記事を参照。
クライアントサイドからPOST送信されてきた画像ファイルを、PHPでサーバーの任意のディレクトリに移動・保存する方法。
- セキュリティーチェックとファイル移動の処理を記述したPHPファイルをサーバーの任意の場所に設置する。
- このPHPファイルを、クライアントサイドの処理でのPOST送信先に指定する。
[ アップローダー全体の処理の流れ ]
- ユーザーによって画像ファイルが選択されたら、画像ファイルをform要素の中身として保持。
- 画像ファイルを保持したフォームをサーバーサイドにPOST送信。
- サーバーサイドで受信したPOSTの内容をセキュリティーチェック。
- 受け取った画像ファイルをサーバー内の所定のディレクトリに移動。
- 処理の完了を表示。
[ クライアントサイドの処理 ]
- HTMLのinput要素かドラッグ・ドロップでローカルファイルから画像ファイルを選択・取得。(ファイル選択の方法の詳細は前の記事を参照。)
- 選択された画像ファイルを、「FormData」オブジェクトを利用してform要素の中にinput要素として保持。
- 「XMLHttpRequest」オブジェクト(または「fetch()」メソッド)でサーバーサイドにPOST送信。
- サーバーサイドからレスポンスを受け取って完了を表示。
[ サーバーサイドの処理 ]
- POSTされてきたフォームの内容をセキュリティーチェック。
- 受け取った画像ファイルをリネームしてサーバー内の所定のディレクトリに移動。
- すべてのファイルの移動が完了したらクライアントサイドに任意のレスポンスを返す。
POST送信されてきた画像ファイルをサーバーに保存
クライアントサイドからPOST送信されてきた画像ファイルを、PHPでサーバーの任意のディレクトリに移動して保存する方法。
- 「php.ini」(PHPの設定ファイル)の「upload_tmp_dir」ディレクティブで他の場所を指定しない限り、POSTされてきたファイルはサーバーのデフォルト設定のテンポラリーディレクトリに一時保存される。(リクエスト先での処理が完了すると一時保存は破棄される。)
- 「POSTメソッドで現在のスクリプトにアップロードされた項目」の連想配列を保持している「$_FILES」(HTTPファイルアップロード変数)を使用して、受け取ったファイルの中身を確認する。(アップロードに失敗している場合は「NULL」が返る。)
※ 「$_FILES」は、PHPの定義済み変数(スーパーグローバル変数)。
※ POST送信でのアップロードについてのPHPの公式ドキュメント。
セキュリティー
ファイルアップロードの処理は脆弱性があるとサーバー自体が攻撃対象になるので、アクセスできるユーザーの制限や受け取ったファイルのチェックなど、実装環境に応じて厳重なセキュリティー対策が必要。
ファイル名や拡張子などのクライアントサイドでのチェックはすべて偽装できるので、サーバーサイドの処理で必ず安全性を確保する。
[ 主な対策方法 ]
- クライアントサイドのアップロードページの閲覧をユーザー認証制にする。(公開する必要が無い場合。)
- サーバーの保存先ディレクトリは非公開ディレクトリにする。(直接アクセスも不可にする。)
- サーバーの保存先ディレクトリとファイルのパーミッションに実行権限を付与しない。(「644」など。)
- 受け取ったファイルは必ずリネームする。(ファイル名による攻撃への対策。)
- ホワイトリスト(許可するリスト)でファイルの拡張子をチェック。(ファイル内のスクリプトが実行できるような拡張子は許可しない。)
- ファイルのバイナリーデータ内のマジックナンバーでファイルのMIMEタイプをチェック。(拡張子の偽装対策。)
- アップロード処理をするPHPファイルの直接閲覧を制限。(htaccessなどで対応。)
- 「Fetch」による送信の場合は、オプションの「integrity」を指定してハッシュ値をチェックして認証。
- 独自にトークンを実装して、トークンをチェック。
PHPファイルの記述
「$_FILES」(ファイルアップロード変数)を使用して、POSTされてきたファイルの内容をチェックし、「move_uploaded_file()」関数で「サーバー上に一時保存されているパス」から「任意のディレクトリ」に移動する。
- 必要に応じてトークンなどのセキュリティー処理。(下記では省略。)
- 「$_FILES[‘input要素のname属性値’]」でクライアントサイドで設定したフィールドが存在するかチェック。(無ければエラー処理。)
- 「画像ファイルの保存先ディレクトリ」、「一度に許可するファイルの上限個数」、「許可するファイルサイズの上限」、「許可する拡張子」を設定。
- 「$_FILES」でフィールドに保持されているファイルの情報を取得。
- 送信されてきたファイルの個数をチェック。(下記では上限を「10」個に設定。)
- ファイル情報にある「エラーコード」をひとつずつチェック。(「0」以外はエラー。公式ドキュメントのエラーコード一覧。)
- それぞれのファイルサイズと拡張子とMIMEタイプをチェック。(指定の画像形式以外ではエラー処理。)
- 「move_uploaded_file()」関数で「一時保存されているパス」から「保存先ディレクトリ」に移動。(下記ではファイル名を現在時刻のタイムスタンプにリネーム。)
- レスポンスとなる任意のメッセージを出力表示。
※ 「move_uploaded_file()」関数のファイル移動で、移動先に同名ファイルがある場合は上書きされるので注意。
※ 「move_uploaded_file()」関数には、HTTPのPOSTでアップロードされたファイルかどうかを検証する処理が含まれている。
※ ファイルのMIMEタイプは、「fileinfo」クラスの「file()」メソッドを利用して確認する。(fileinfoクラスのPHPの公式ドキュメント。)
//必要に応じてセキュリティーチェックを実行
//使用する変数を設定
//レスポンス用のメッセージを保持
$ag2message;
//使用する定数を定義
//クライアント側で設定してあるフィールド名
const AG2FIELD_NAME = 'ag2postfile';
//ファイル変数の有無をチェック
if(!isset($_FILES[AG2FIELD_NAME])){
$ag2message = 'Error : 正しく送信できませんでした。';
exit($ag2message);
}
//使用する定数を定義
//サーバーでの保存先ディレクトリ
const AG2UPLOAD_DIR = 'uploads/images/';
//一度に許可するファイルの上限個数
const AG2MAXIMUM_FILENUM = 10;
//許可するファイルサイズの上限を指定
const AG2MAXIMUM_FILESIZE = 104857600;//100MB
//許可する拡張子を正規表現で指定 (フラグ「i」で大文字小文字区別無し)
const AG2REGEX_EXTENSION = '/png$|jpg$|jpeg$|gif$/i';
//受け取ったファイルの情報を取得
$AG2FILES = $_FILES[AG2FIELD_NAME];
$ag2file_name = $AG2FILES['name'];//ファイル名
$ag2file_type = $AG2FILES['type'];//ファイルのMIMEタイプ
$ag2file_size = $AG2FILES['size'];//ファイルサイズ
$ag2file_tmp_name = $AG2FILES['tmp_name'];//サーバーで一時保存されているパス
$ag2file_error = $AG2FILES['error'];//エラーコード
$ag2file_num = count($ag2file_error);//ファイルの数
//ファイル数をチェック
if($ag2file_num > AG2MAXIMUM_FILENUM){
$ag2message = 'Error : 一度にアップロードできるファイルの数は'.AG2MAXIMUM_FILENUM.'個以下です。';
exit($ag2message);
}
//受け取ったすべてのファイルをひとつずつチェックして保管処理
foreach($ag2file_error as $k => $v){
switch($v){
case 0: //エラー無しの場合
//個々のファイルサイズをチェック
if($ag2file_size[$k] > AG2MAXIMUM_FILESIZE){
$ag2message = 'Error Code 0 : アップロードできるファイルのサイズは100MB以下です。';
break;
}
//ファイル名を取得 (ディレクトリ部分を削除)
$this_name = basename($ag2file_name[$k]);
//ファイルの拡張子を取得 (ファイル名部分を削除)
$this_ext = pathinfo($this_name, PATHINFO_EXTENSION);
//マジックバイトシーケンスからMIMEタイプを取得
$ag2finfo = new finfo();
$this_mime_check = $ag2finfo->file($ag2file_tmp_name[$k], FILEINFO_MIME_TYPE);
$this_mime_check = str_replace('image/', '', $this_mime_check);
//ファイルの拡張子をチェック
if(!preg_match(AG2REGEX_EXTENSION, $this_ext) || !preg_match(AG2REGEX_EXTENSION, $this_mime_check)){
$ag2message = 'Error Code 0 : 画像ファイル以外はアップロードできません。';
break;
}
//現在時刻 (デフォルトタイムゾーンでの現在のUnixタイムスタンプ)
$current_stamp = time();
//リネームしてファイルを移動
move_uploaded_file($ag2file_tmp_name[$k], AG2UPLOAD_DIR.'img_'.$current_stamp.'_'.$k.'.'.$this_ext);
$ag2message = ($k + 1).'枚目の画像をアップロードしました。';
break;
case 1: //php.iniで設定されている「upload_max_filesize」を超えている場合
$ag2message = 'Error Code 1 : ファイルサイズが上限を超えています。';
break;
case 2: //HTMLのフォームで指定された「MAX_FILE_SIZE」を超えている場合
$ag2message = 'Error Code 2 : ファイルサイズが上限を超えています。';
break;
case 3: //ファイルの一部しかアップロードされていない場合
$ag2message = 'Error Code 3 : ファイルのアップロードに失敗しました。';
break;
default: //それ以外のエラーはデフォルト
$ag2message = 'Error Code 4 : アップロードできませんでした。';
}
//個々のファイルのレスポンスのメッセージ
echo $ag2message.PHP_EOL;
}
//すべての工程が完了したあとのメッセージ
echo 'アップロード処理が完了しました。';
isset(‘変数’) : 指定された変数が宣言されていて、かつ値がnullでない場合にtrueを返す。true以外の場合はfalseを返す。
exit(‘文字列’または’数値’) : 指定した文字列をメッセージとして出力し、現在のスクリプトを終了する。引数に数値を指定した場合、その値が終了ステータスとして使用され、表示出力はされない。(数値として終了ステータスに指定できるのは0~254の間の値。)
count(‘配列’) : 指定した配列(またはCountableオブジェクト)に含まれる要素の数を返す。
basename(‘ファイルパス’, ‘接尾語’) : 指定したファイル(またはディレクトリ)へのパスを含む文字列を受け取って、文字列の最後にあるファイル名の部分だけを返す。指定した接尾語(拡張子など)でファイル名が終了している場合は、ファイル名から接尾語を削除した値が返される。(ファイルパスは文字列として扱われるので、相対パスの場合、「../」などは展開されない。)
pathinfo(‘ファイルパス’, ‘フラグ’) : 指定したファイルパスに関する指定したフラグの情報を文字列で返す。フラグを省略した場合はすべての要素(dirname、basename、extension、filename)が連想配列で返される。(ファイルパスは文字列として扱われるので、相対パスの場合、「../」などは展開されない。)
pathinfoのフラグ指定は以下の4つのいずれか。
「PATHINFO_DIRNAME」で、dirname(basenameより前までのパス)を返す。
「PATHINFO_BASENAME」で、basename(指定したパスの最後のスラッシュ以降の文字列)を返す。(basenameの中にドットが複数含まれる可能性がある。)
「PATHINFO_EXTENSION」で、extension(拡張子)を返す。複数の拡張子が含まれる場合、最後の拡張子だけを返す。(拡張子が無ければextensionは返さない。)
「PATHINFO_FILENAME」で、filename(basenameとextensionの部分のみ)を返す。basenameにドットが複数ある場合、最後のドットより前のドット部分は含まれない。
finfo() : finfoクラス。任意のファイルに関する情報を取得する。
file(‘ファイル名’, ‘フラグ’) : finfoクラスが持つメソッド。指定したファイルについての指定したフラグの情報を取得して返す。フラグは省略可。
str_replace(‘置換前文字列’, ‘置換後文字列’, ‘対象文字列’) : 対象文字列内のすべての置換前文字列を置換後文字列で置換。文字列ではなく配列の指定も可能。返り値はすべての該当箇所を置換した後の対象文字列。
preg_match(‘正規表現’, ‘対象文字列’, ‘変数’, ‘フラグ’) : 対象文字列内で最初に指定した正規表現とマッチした文字列(マッチした文字列の全体と、後方参照できるようにグループ化された文字列のマッチした部分)を指定した変数に配列で格納する。返り値は、マッチすれば1、しなければ0、失敗した場合はfalseを返す。
time() : 現在のUnixタイムスタンプを返す。
move_uploaded_file(‘一時保存されているパス’, ‘サーバー内の保存先パス’) : アップロードされて一時保存されているファイルを、サーバー内の指定した保存先のディレクトリに移動する。移動先のパスに同名ファイルが既に存在する場合は上書きされる。成功すればtrue、有効なアップロードファイルでない場合(または何らかの理由で移動できない場合)には処理は行われずにfalseが返る。
- ファイルをアップロードする
- 本当は怖いファイルアップロード攻撃の理解と修正方法(PHP編)
- ファイルのMIMEタイプを取得して拡張子を判断する方法
- ファイルのMIMEタイプを確認する方法・finfo_file
https://memo.ag2works.tokyo/post-4486/
画像アップローダーの実装。(サーバーサイド) | memo メモ [AG2WORKS]
<a href="https://memo.ag2works.tokyo/post-4486/" target="_blank" rel="noopener">画像アップローダーの実装。(サーバーサイド) | memo メモ [AG2WORKS]</a>
この記事へのコメント
コメントの書き込みはまだありません。