【PHP】アップロードされるファイルの拡張子はアプリケーション側で制限するか変更するかした方がいい理由

  • 2022年11月4日
  • 2022年11月4日
  • PHP

 端的に言うと特定の拡張子を付けられたファイルを公開領域に置かれただけでリモートコード実行の脆弱性になりやすいためです。より具体的には公開領域に拡張子が .php のファイルを置かれた時点でその .php ファイルへのアクセス時にその .php ファイルが PHP として実行されやすいです。

 どの様な流れで公開領域に置かれた .php ファイルがリモートコード実行されるかは昔ながらの HTML と PHP が混在した .php ファイルで構成された静的ページにアクセスするのをイメージすると連想しやすいです。PHP はコンパイル抜きに実行できるインタプリタ言語であり、<?php?>でくくった外を無視します。加えてPHPを実行する目的で配置された多くの web サーバーが .php の拡張子のファイルにアクセスされた時点でそのファイルを PHP として実行します。これにより .php の拡張子を持つファイルは他言語に比べて大分自由なコードで公開領域に配置するだけで実行されます。このつくりはパパっと web ページを作って公開するのに便利なつくりですが、攻撃の起点にされやすい部分でもあります。

PHP: PHP タグ – Manual

 よくあるファイルアップロードのバリデーションに MIME Content-type の検証がありますが、これだけでは不十分です。これはファイルのMIME Content-typeを検出する関数 mime_content_type を用いて、アップロードを許すか許さないかを判別する処理です。これを素朴に書くと例えば次の様になります。

<form action="" method="post" enctype="multipart/form-data">
    <input type="file" name="profile-img">
    <button type="submit">送信</button>
</form>
<?php

if(isset($_FILES['profile-img'])) {
    // POSTされたファイルの MIME Content-type を取得
    // @see https://php.net/manual/ja/function.mime-content-type.php
    $mime = mime_content_type($_FILES['profile-img']['tmp_name']);
    // MIME Content-type が PNG 画像であることを示すならばOK
    if($mime === 'image/png'){
        // バリデーション問題なしで公開領域に保存
        rename($_FILES['profile-img']['tmp_name'],__DIR__."/profile_img/".$_FILES['profile-img']['name']);
        echo "ファイルアップロード完了";
    }else{
        echo 'バリデーションエラー発生';
    }
}

 アップロードされたファイルの MIME Content-type を PHP 側で検出してファイルが想定されたものであるか否かを確認します。一見これのみでも問題なさそうですが、PHP としても別の形式としても適正なファイルに対応できていません。これは例えば次の様なファイルです。

 PNG画像として画像を開け、PHPタグで括ったコードを持つファイルです。これはファイルの中身がPNG画像として正しいため MIME Content-type によるPNG画像であるか否かのチェックを通ります。このためファイルが保存されます。この時、拡張子を .php として保存すればアクセス時、次の様に PHP として実行されます。これによりリモートコード実行が可能になってしまいます。

 対策方法は主に二種類あります。一つは拡張子が PHP として実行されるファイルを公開領域に保存しないことです。単にファイル名の末尾が PHP として実行される形式か否かのチェックでもいいですし、MIME Content-Type から推察される拡張子に置き換える方法でもいいです。これは例えば次です。

/* PHP として実行される形式か否かのチェック例 */
// 保存されるファイルについて名前が php でなく、php として実行される拡張子も持たない
$hasNotPhpExt = trim(strtolower($filename)) !== 'php' && !in_array(trim(strtolower(pathinfo($filename, \PATHINFO_EXTENSION))), [
        'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar'
    ]);

/* MIME Content-Type から推察される拡張子に置き換える例 */
// $fileName に保存用の名前、$filePath に保存予定のファイルの実体のパスがあるとして
// ファイルの中身からあるべき拡張子を取得
$ext = $finfo->file($filePath);
if (! preg_match("/\.{$ext}$/i", $fileName)) {
    // 正規表現のiフラグは大文字小文字を無視するフラグ
    // もしファイル末尾の拡張子がファイルの中身にそぐう拡張子でない場合は
    // 拡張子を付け足したファイルとして名前を変更
    rename($filePath, $filePath.'.'.$ext);
}

 こんな感じでそもそも PHP として実行できない様にすることができます。この方法はPHPフレームワークの Laravel でも採用されています。

framework/ValidatesAttributes.php at 9.x · laravel/framework#L1385-L1405

    /**
     * Check if PHP uploads are explicitly allowed.
     *
     * @param  mixed  $value
     * @param  array<int, int|string>  $parameters
     * @return bool
     */
    protected function shouldBlockPhpUpload($value, $parameters)
    {
        if (in_array('php', $parameters)) {
            return false;
        }

        $phpExtensions = [
            'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
        ];

        return ($value instanceof UploadedFile)
           ? in_array(trim(strtolower($value->getClientOriginalExtension())), $phpExtensions)
           : in_array(trim(strtolower($value->getExtension())), $phpExtensions);
    }

 Laravel の場合、ファイルや画像のバリデーションをする時に MIME Content-type のチェックとまとめてこの shouldBlockPhpUpload メソッドが呼ばれる様になっており、開発者が自然に安全なプログラムを組めるようになっています。

// @see https://github.com/laravel/framework/blob/0702f556f72745f34570fbdfa8e37b8ea0b7ffa5/src/Illuminate/Validation/Concerns/ValidatesAttributes.php#L1362-L1383
    /**
     * Validate the MIME type of a file upload attribute is in a set of MIME types.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @param  array<int, int|string>  $parameters
     * @return bool
     */
    public function validateMimetypes($attribute, $value, $parameters)
    {
        if (! $this->isValidFileInstance($value)) {
            return false;
        }
// MIMEバリデーションのメソッドでもここでPHP拡張子防御メソッドを呼んでます
        if ($this->shouldBlockPhpUpload($value, $parameters)) {
            return false;
        }

        return $value->getPath() !== '' &&
                (in_array($value->getMimeType(), $parameters) ||
                 in_array(explode('/', $value->getMimeType())[0].'/*', $parameters));
    }

 もう一つの方法は web サーバーに PHP ファイルを実行させない方法です。こちらであればPHPとして実行される拡張子のファイルをアップロードされても問題ないです。例えば Apache では次の様な .htaccess を公開領域に置くことで、その .htaccess が置いてあるディレクトリ以下における PHP の実行を防げます。

php_flag engine off

PHP: 実行時設定 – Manual
 こちらは .htaccess の改ざんを防ぐためにまた防御が必要ですが、これでも防げます。プログラムのソースコードの外を触る必要がありますが、Apache の設定ファイル本体に

    <Directory "${APACHE_DOCUMENT_ROOT}/assets">
        php_flag engine off
    </Directory>

の様に書いて特定のディレクトリ以下で PHP を実行できなくする方法もあります。
 また少々手間ですが、そもそも公開領域に置かず、あるディレクトリ以下のファイルを常にダウンロードをさせるプログラムを介する方法もあります。

 この様にファイルアップロード時にはそのファイルの中身のみでなく拡張子のバリデーションも必要になり、PHPのプログラムであればそれは特に重要です。この記事では公開領域にファイルを置いた場合のみを紹介しましたが実際の PHP では更に include, require といったファイルの読み込み機能があり、そこを狙われた場合も危険です。この記事で紹介した攻撃方法は昔からあるので古い記事だからといって無視せず色々読んでみると参考になります。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG