Help us understand the problem. What is going on with this article?

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

この記事を理解するには、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 アプリのコードを一緒に読み解こう

7968
学んだことを投稿していきます。誤りがあればご指摘ください。 note でも記事を投稿しています。
https://note.com/7968
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした