PHPでMIME-Typeを判定する方法

サイト作成時にユーザーからファイルをアップロードしてもらう場合に気をつけることとして「それが正しいフォーマットかどうか」という点があります。

つまり偽装ファイルでないかどうかということですね。 これを判定するにはMIME-Typeを調べるのが良いとされています。

Webブラウザーからの情報は信用しない=基本

正しいファイルフォーマットを知るためには、ファイルのMIME-Type情報を取得して判断するのですが、ブラウザーがサーバーに伝えるMIME-Typeはウソの情報を与えることもできます。

Webブラウザは拡張子を変えるだけで騙される!?

突然ですが、適当なテキストファイルを作成して拡張子を「.gif」に変えてみてください。 Windowsだと警告がでますが、無視してOKするとテキストファイルが画像アイコンに変わります。

これだけでWebブラウザーは「画像だ」と認識してしまいます。 MIME-Typeでいうと「image/gif」になっちゃうんですね。 ヤバいですよね。

ですから、次のような「$_FILES」配列の「type」を見てMIME-Typeを判定するのは絶対に止めておきましょう。

$mime = $_FILES["myfile"]["type"];  // 嘘のMIME-Typeが入ってしまうので×

アップロードファイルの形式チェックはサーバー上で!

アップロードしたファイルタイプの判定はサーバー上で行いましょう。

mime_content_type() や FileInfoモジュールは使わない

PHPではmime_content_type()関数(公式では非推奨)や、標準搭載されていないFileInfoモジュールを使うとMIME-Typeを判定することができますが、場合によってはこれらの方法が使えないこともあると思います。

そこで、ここではより多くのサーバーで利用できるように汎用的な方法をご紹介します。

どうすればいいかというと、アップロードされたファイルをサーバー上でコマンドを使って判別しましょう。

サーバーがLinuxであれば「file」コマンドという便利な命令があります。 これにオプションで「bi」をつけて実行することでMIME-Typeを判別してくれます。

PHPだと以下のようにします。 まず入力フォームを以下のようにしてみます。

<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="myfile" />
<input type="submit" value="送信" />
</form>

そして、受信先の upload.php を以下のようにしてみましょう。

<?php
$path = $_FILES['myfile']['tmp_name'];
$mime = shell_exec('file -bi '.escapeshellcmd($path));
$mime = trim($mime);
$mime = preg_replace("/ [^ ]*/", "", $mime);
print($mime);
?>

これで$type変数にMIME-Typeが格納されているので、正しい判定が行えます。

trim()関数を適用させている理由は、shell_exec()関数で帰ってきた文字列に改行コードが含まれているので、それを除去するためです。 また「file -bi」で取得したMIME-TypeにはMIME-Typeが2つ記載されたり、文字コードが含まれたりするので、空白以降をpreg_replace()で除去します。

次に、一般的に利用されるファイルタイプのMIME-Typeリストをご紹介しましょう。

画像系

拡張子 MIME-Typeリスト
GIF image/gif
image/x-xbitmap
image/gi_
JPG または JPEG image/jpeg
image/jpg
image/jp_
application/jpg
application/x-jpg
image/pjpeg
image/pipeg
image/vnd.swiftview-jpeg
PNG image/png
application/png
application/x-png
BMP image/bmp
image/x-bmp
image/x-bitmap
image/x-xbitmap
image/x-win-bitmap
image/x-windows-bmp
image/ms-bmp
image/x-ms-bmp
application/bmp
application/x-bmp
application/x-win-bitmap

ドキュメント系

拡張子 MIME-Typeリスト
PDF application/pdf
application/x-pdf
application/acrobat
applications/vnd.pdf
text/pdf
text/x-pdf

圧縮ファイル系

拡張子 MIME-Typeリスト
ZIP application/zip
application/x-zip
application/x-zip-compressed
application/octet-stream
application/x-compress
application/x-compressed
multipart/x-zip

Microsoft Office系のファイルはMIME-Typeも間違われる!?

実は、この方法だとWord, Excel, PowerPoint などのOffice系のファイルを正しく判定できません。(Microsoft は度々厄介な仕様を作るものですね。。) Office2003も、Office2007以降に登場した docx, xlsx, pptx といった新しく登場した拡張子も間違って判定されてしまいます。

これらを正しく判定するには、中身のバイトを検索して正しいバイト列になっているかどうか、といった複雑な判定になります。

バイト列をチェックする方法

バイト列を正確に見て判断できれば、Office2003系のファイルタイプを判断することができます。 判断方法は、通常のファイルハンドルを取得し、指定の開始バイトまでファイルポインタを移動させ、fread()で10バイトほど読み込みます。 それをbin2hex()で16進数に変換して比較します。

以下の例は512バイト目までファイルポインタを移動させて、10バイトを16進数文字列に変換しています。

if (($fh = fopen($path, "rb")) !== FALSE) {
    fseek($fh, 512);
    $hex = strtoupper(bin2hex(fread($fh, 10)));
}

Office2003以前のファイルはMIME-Typeのみで判別不可

Office2003以前のファイルについては、ExcelやPowerPointもすべて「application/msword」と判定されるので、以下の表に従ってバイト列を比較して判断しましょう。

Office2003以前のファイルは0バイト目から確認しても全部同じバイト列なので、Office系かどうかを判断するだけの分岐に利用し、ファイルタイプを比較するためには512バイト目からのバイト列を比較してください。

たとえば、Excelシート (拡張子:xls)のファイルかどうかを判断したければ、次のように正規表現を作成して判断します。

if (($fh = fopen($path, "rb")) !== FALSE) {
    // 0バイト目からの判定
    fseek($fh, 0);
    $hex = strtoupper(bin2hex(fread($fh, 10)));
    if (preg_match("/^D0CF11E0A1B11AE100.*/", $hex)) {
        // 512バイト目からの判定
        fseek($fh, 512);
        $hex = strtoupper(bin2hex(fread($fh, 10)));
        if (preg_match("/^(?:FDFFFFFF(?:(?:10|1F|22|23|28|29)02|20000000)|0908100000060500).*/", $hex)) {
            print("Excelのファイルです。");
        }
    }
    fclose($fh);
}

Office2003以前用ファイルタイプ判別表

拡張子 MIME-Typeリスト 開始バイト バイト列16進数リスト
DOC application/msword (公式)
application/doc
appl/text
application/vnd.msword
application/vnd.ms-word
application/winword
application/word
application/x-msw6
application/x-msword
0 D0CF11E0A1B11AE100
512 ECA5C100
XLS application/vnd.ms-excel (公式)
application/msexcel
application/x-msexcel
application/x-ms-excel
application/vnd.ms-excel
application/x-excel
application/x-dos_ms_excel
application/xls
0 D0CF11E0A1B11AE100
512 FDFFFFFF1002
FDFFFFFF1F02
FDFFFFFF2202
FDFFFFFF2302
FDFFFFFF2802
FDFFFFFF2902
FDFFFFFF20000000
0908100000060500
PPT application/vnd.ms-powerpoint (公式)
application/mspowerpoint
application/ms-powerpoint
application/mspowerpnt
application/vnd-mspowerpoint
application/powerpoint
application/x-powerpoint
application/x-m
0 D0CF11E0A1B11AE100
512 FDFFFFFF0E000000
FDFFFFFF1C000000
FDFFFFFF43000000
FDFFFFFF10000000
006E1EF0
A0461DF0

Office2007以降のファイルはバイト列でも無理

Office2007以降のOffice系ファイル(docx, xlsx, pptx など)のMIME-Typeは実はすべて「application/zip」や「application/x-zip」と判断されてしまう。
中身を見るとほんとうにzipファイルなのです。 ですので、この判定をされたら拡張子を調べてそれぞれのファイルのMIME-Type情報に変更しましょう。

9/5追記:Office2007以降のファイルでも判定する方法を書きました。

おまけ

最後に、各ファイルがどういったものか、MIME-Typeは何なのかを調べるためのサイトを2つご紹介しておきましょう。 どちらも英語ですが、特にFilext.comはいろいろと参考になると思いますよ。

このページをシェアする

1 件のコメント

  • shtr より:

    こんばんは。記事、とても参考になりました。
    確かに、$_FILESとかは信頼できんですよね。どうしても、ファイルアップロードって、神経質にならなければならないし、でもアップロードのニーズはどうしてもあるし、なので、悩ましいと思います。

    ところで、プラットフォームによっては、fileコマンドのオプション指定が異なる場合があるので、注意がいるかと思い、コメント残します。
    例えば、OSXで開発している場合などでは(まさに僕がそうです)、file -bi hogehoge.jpgとかすると、mime-typeが表示されず、単に”regular file”が返されます。テスト環境が OSXの場合は注意が必要です。

    他のプラットフォームについては調べていないのですが、ケースバイケースで、アプリの本番環境に合わせたチェックの実装が必要となるかと。

    では!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

2011-04-27