LoginSignup
29
24

More than 3 years have passed since last update.

【PHP】なぜ、$_FILES['userfile']['type'] の値は信用できないの?

Last updated at Posted at 2017-04-14

この記事を理解するには、HTTPの基本的な理解が必要です。
リクエスト?レスポンス?何それ?という方は、ここ をご一読ください。

PHPマニュアルの POST メソッドによるアップロード には下記の説明があります。

$_FILES['userfile']['type']
ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、"image/gif" のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。

引用:POST メソッドによるアップロード

$_FILES['userfile']['type'] は、ファイルの MIME 型、つまりファイルの種類を表します。
MIMEタイプは タイプ/サブタイプ のように / で区切って表します。
各ファイルごとにMIMEタイプは決まっています。

ファイル MIMEタイプ
GIF image/gif
JPEG image/jpeg
PNG image/png
HTML text/html
CSS text/css
JavaScript text/javascript

MIMEタイプは、Internet Assigned Numbers Authority(IANA、アイアナ)という組織が管理しています。
Media Types に一覧で掲載されています。

$_FILES['userfile']['type'] は、ブラウザが知らせてくれたファイルの種類をもとにしています。
具体的にいうとリクエストメッセージにあるMIMEタイプをもとにしています。
実際に Fidder を使って、どのようなリクエストメッセージになっているのか確認していきます。
今回は、Windows7のXAMPP環境でGoogleChromeを使って確認します。

ファイルを選択し、送信すると、$_FILES['userfile']['type'] を表示してくるページを作成します。

php-file-none.png

PHP
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP</title>
</head>
<body>
<form  method="post" enctype="multipart/form-data">
    <p>Pictures:
        <input type="file" name="pictures">
        <input type="submit" value="送信">
    </p>
</form>
<?php
if(isset($_FILES['pictures'])) {
    echo $_FILES['pictures']['type'];
}
?>
</body>
</html>

JPEG なら image/jpeg 、GIFなら image/gif、PNGなら image/png と表示されるはずです。
実際に JPEG ファイルを選択して、送信してみます。

php-file.png

予想通り、image/jpeg と表示されました。
このときのリクエストメッセージは下記です。

HTTP(リクエストメッセージ)
POST http://localhost/test/ HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 64201
Cache-Control: max-age=0
Origin: http://localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7c22X5usFk6Z8VCU
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost/test/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

------WebKitFormBoundary7c22X5usFk6Z8VCU
Content-Disposition: form-data; name="pictures"; filename="01.jpg"
Content-Type: image/jpeg

     以下、画像データ(バイナリ)

注目すべきは、メッセージボディの Content-Type: image/jpeg です。
MIMEタイプが記述されてますね。
$_FILES['userfile']['type'] の値は、このリクエストメッセージの Content-Type: image/jpeg をもとにしていたということです。

この結果を見て、一つ気になる点があります。
ブラウザはこの Content-Type: image/jpeg をどのように判断したのでしょうか?
確かめて見ましょう。

テキストエディタを開いて、下記のようにscriptを記述して、xss.png と名前をつけて保存します。

xss.png
<script>alert("xss");</script>

拡張子は png になっていますが、中にはscriptタグが書かれています。
このファイルを選択して、送信してみましょう。

php-file-png.png

image/png と表示されました。
このときのリクエストメッセージは下記です。

HTTP(リクエストメッセージ)
POST http://localhost/test/ HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 214
Cache-Control: max-age=0
Origin: http://localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarySqOUDVmXAGnTyQYn
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost/test/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

------WebKitFormBoundarySqOUDVmXAGnTyQYn
Content-Disposition: form-data; name="pictures"; filename="xss.png"
Content-Type: image/png

<script>alert("xss");</script>
------WebKitFormBoundarySqOUDVmXAGnTyQYn--

メッセージボディには、Content-Type: image/png とあります。

xss.png というファイル名を xss.jpg に変更して、送信すると、メッセージボディの Content-Typeimage/jpeg になりました。

リクエストメッセージの MIMEタイプ は、拡張子をもとにしているため、全く信用できないということです。

そもそも、今回の Content-Type に限らず、リクエストメッセージ全体を偽装することが可能なので、リクエストメッセージで送られてくる値は全て信用できません。
そのため、サーバーサイド(PHP)側で確認する必要があります。

他の Firefox、Opera でも確認しましたが、拡張子にあわせてMIMEタイプが変わりました。

あれ... Internet Explorer がないよね...

Internet Explorer 11 の場合は?

Internet Explorer 11 で xss.jpg を選択して送信したときのリクエストメッセージが下記です。

HTTP(リクエストメッセージ)
POST http://localhost/test/ HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://localhost/test/
Accept-Language: ja-JP
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: multipart/form-data; boundary=---------------------------7e125bc25117e
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 245
DNT: 1
Host: localhost
Pragma: no-cache

-----------------------------7e125bc25117e
Content-Disposition: form-data; name="pictures"; filename="C:\Users\7968\Desktop\xss.jpg"
Content-Type: text/plain

<script>alert("xss");</script>
-----------------------------7e125bc25117e--

メッセージボディにある Content-Typetext/plain です。
xss.png に拡張子を変更しても、text/plain でした。

Internet Explorer には、ファイルの中身から、MIMEタイプを推測する機能があります。
おそらくですが、それが機能して text/plain と判断したのではないかと思います。

Internet Explorer の場合は MIMEタイプ が他のブラウザと異なりますが、どちらにせよ、サーバーサイド(PHP)側でチェックする必要があります。

続き:【PHP】アップロードされたファイルの種類をどうやって確認するの?

note

note でも記事を公開してるので、興味がある方はご覧ください。

【初学者向けコードリーディング】 PHP の TODO アプリのコードを一緒に読み解こう

29
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
24