今回はPHPで画像を表示させる時に保存ディレクトリを隠蔽する方法を紹介したいと思います。
今回のコードで画像URLが下記のように変更されます。
// 例:変更前(通常のディレクトリ表示) <img src="https://example.com/upload/img/1/001.jpg" alt="img001"> // 例:変更後(保存ディレクトリを隠蔽) <img src="https://example.com/image.php?p=1&f=001&e=jpg" alt="img001">
Invisible image url

 画像の保存しているディレクトリを隠蔽することで以下の利点が得られます。
- 微量のセキュリティ向上
- 画像自体に認証機能を設けることができる
- ドキュメントルートより上の階層に画像を配置することが可能になる
特定のユーザーのみに画像を表示させたい場合に認証機能を付与できるのでとても便利だと思います。
また、ドキュメントルート外に画像を置くことができるようになります。
ソースコード
コードを下記に記します。
<?php 
// =========================================================== 
// If you require access restrictions, you can write here. 
// =========================================================== 
// option param 
const FILE_REGEX = '@\A[a-z0-9_-]+\z@ui'; 
const EXT_REGEX = '@\A[a-z]+\z@u'; 
const ALLOW_MIME_TYPE = [ 
  'gif' => 'image/gif',
  'jpg' => 'image/jpeg',
  'png' => 'image/png',
];
// get query param
$path = (int)filter_input(INPUT_GET, 'p');
$input_name = (string)filter_input(INPUT_GET, 'f');
$input_ext = strtolower((string)filter_input(INPUT_GET, 'e'));
// switching img path
if ($path === 1) {
  $img_path = __DIR__ . 'path_to_img1';
} elseif ($path === 2) {
  $img_path = __DIR__ . 'path_to_img2';
} else {
  header('Content-Type: text/plain; charset=UTF-8', true, 400);
  exit('No such image.');
}
// ===========================================================
// If you require access restrictions for each image,
// you can write here.
// ===========================================================
// validation
if (preg_match(FILE_REGEX, $input_name) && preg_match(EXT_REGEX, $input_ext)) {
  $input_file = sprintf('%s.%s', $input_name, $input_ext);
  $finfo = new finfo(FILEINFO_MIME_TYPE);
  $mime_type = $finfo->file($img_path . $input_file);
  // response
  if ($ext = array_search($mime_type, ALLOW_MIME_TYPE, true)) {
    $filename = sprintf('%s.%s', $input_name, $ext);
    header('Content-Disposition: inline; filename="' . $filename . '"', true);
    header('Content-type:' . $mime_type, true);
    readfile($img_path . $filename);
    exit();
  }
}
// error handler
header('Content-Type: text/plain; charset=UTF-8', true, 400);
exit('No such image.');
解説
// get query param $path = (int)filter_input(INPUT_GET, 'p'); $input_name = (string)filter_input(INPUT_GET, 'f'); $input_ext = strtolower((string)filter_input(INPUT_GET, 'e'));
まずGETクエリを3つ受け取ります。
pは画像ファイルまでのパスを切り替えるための数値、fはファイル名、eは拡張子になっています。
画像ファイルへのパスが1つの場合はpパラメータを削除して問題ありません。
 また、eパラメータもアップロード時に拡張子を統一させているのであれば設定する必要はないかもしれません。
// validation
if (preg_match(FILE_REGEX, $input_name) && preg_match(EXT_REGEX, $input_ext)) {
次に画像ファイル名や拡張子が任意の正規表現パターンを満たしているかチェックします。
basename()関数が存在しますが、今回は許可した文字列のみを通すという正規表現を使用しました。いずれにしてもディレクトリトラバーサル問題等が存在するため検証をしっかり行っておく必要があります。
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($img_path . $input_file);
// response
if ($ext = array_search($mime_type, ALLOW_MIME_TYPE, true)) {
  $filename = sprintf('%s.%s', $input_name, $ext);
  header('Content-Disposition: inline; filename="' . $filename . '"', true);
  header('Content-type:' . $mime_type, true);
  readfile($img_path . $filename);
  exit();
}
ファイル名・拡張子に問題がなければ該当するファイルが存在するかを確認し、mimetypeを取得します。header()関数にてContent-typeを正確に出力するためです。
minetypeが許可した物であればreadfile()関数で出力するのですが、このテクニックの最大のポイントがheader('Content-Disposition: inline; filename="' . $filename . '"', true);になります。
このfilename属性を任意のものに設定することで画像の保存ディレクトリを隠蔽することが可能になっています。
まとめ
画像の保存ディレクトリ隠蔽や画像毎に認証機能を設けることができるため、非常に便利なものになっていると思います。
日々、進化していく技術を私達と一緒に共有していきませんか?
 職場で待っています!
最後まで読んでいただきありがとうございました。
