この記事を理解するには、HTTPの基本的な理解が必要です。
リクエスト?レスポンス?何それ?という方は、ここ をご一読ください。
PHPマニュアルの POST メソッドによるアップロード には下記の説明があります。
$_FILES['userfile']['type']
ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、"image/gif" のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 この値は信用できません。
$_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']
を表示してくるページを作成します。
<!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 ファイルを選択して、送信してみます。
予想通り、image/jpeg
と表示されました。
このときのリクエストメッセージは下記です。
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
と名前をつけて保存します。
<script>alert("xss");</script>
拡張子は png
になっていますが、中にはscriptタグが書かれています。
このファイルを選択して、送信してみましょう。
image/png
と表示されました。
このときのリクエストメッセージは下記です。
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-Type
は image/jpeg
になりました。
リクエストメッセージの MIMEタイプ は、拡張子をもとにしているため、全く信用できないということです。
そもそも、今回の Content-Type
に限らず、リクエストメッセージ全体を偽装することが可能なので、リクエストメッセージで送られてくる値は全て信用できません。
そのため、サーバーサイド(PHP)側で確認する必要があります。
他の Firefox、Opera でも確認しましたが、拡張子にあわせてMIMEタイプが変わりました。
あれ... Internet Explorer がないよね...
Internet Explorer 11 の場合は?
Internet Explorer 11 で xss.jpg
を選択して送信したときのリクエストメッセージが下記です。
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-Type
は text/plain
です。
xss.png
に拡張子を変更しても、text/plain
でした。
Internet Explorer には、ファイルの中身から、MIMEタイプを推測する機能があります。
おそらくですが、それが機能して text/plain
と判断したのではないかと思います。
Internet Explorer の場合は MIMEタイプ が他のブラウザと異なりますが、どちらにせよ、サーバーサイド(PHP)側でチェックする必要があります。
続き:【PHP】アップロードされたファイルの種類をどうやって確認するの?
note
note でも記事を公開してるので、興味がある方はご覧ください。