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

この記事を理解するには、HTTPの基本的な理解が必要です。
リクエスト？レスポンス？何それ？という方は、[ここ](http://qiita.com/7968/items/4bf4d6f28284146c288f) をご一読ください。


PHPマニュアルの [POST メソッドによるアップロード](http://php.net/manual/ja/features.file-upload.post-method.php) には下記の説明があります。

> $_FILES['userfile']['type']
> ファイルの MIME 型。ただし、ブラウザがこの情報を提供する場合。 例えば、"image/gif" のようになります。 この MIME 型は PHP 側ではチェックされません。そのため、 **この値は信用できません。**
> 
> 引用：[POST メソッドによるアップロード](http://php.net/manual/ja/features.file-upload.post-method.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](https://www.iana.org/assignments/media-types/media-types.xhtml) に一覧で掲載されています。

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

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

![php-file-none.png](https://qiita-image-store.s3.amazonaws.com/0/72985/39ad819c-81a4-153d-bc02-619f81fca97e.png)

```php: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](https://qiita-image-store.s3.amazonaws.com/0/72985/f14afefc-3ec8-ae37-b92c-90bd5a3c07dd.png)

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

```http: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` と名前をつけて保存します。

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

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

![php-file-png.png](https://qiita-image-store.s3.amazonaws.com/0/72985/eba25746-67af-c8c8-8020-955a281e2400.png)

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

```http: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-Type` は `image/jpeg` になりました。

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

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

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

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

# Internet Explorer 11 の場合は？

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

```http: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-Type` は `text/plain` です。
`xss.png` に拡張子を変更しても、`text/plain` でした。

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

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

続き：[【PHP】アップロードされたファイルの種類をどうやって確認するの？](http://qiita.com/7968/items/e4b3adeb9832a9c32c52)

# note

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

・[【初学者向けコードリーディング】 PHP の TODO アプリのコードを一緒に読み解こう](https://note.mu/7968/n/n46a8beb87c93)