PHPを用いた入力フォームの「ファイルアップロードサイズ制限」にまつわる覚書

初Qiita...ども...
俺みたいなエンジニア三年目間近でこんな戒め記事書く人間、他にいますかっていねーか、はは

backlogで来ていたこの前のクライアントの依頼
「フォームにサイズ制限を超えた添付をしてきた奴はエラーメッセージ出したフォームに戻してほしい」とか
ま、それが普通ですわな

かたや俺は電子の砂漠で技術記事みて、電話返すんすわ。「割とすぐいけると思います」
It's a death flag.
猪突猛進？　それ、褒め言葉ね。

適当に添付したファイルのサイズ: 2GB
var_dumpで見たPOSTデータの中身: array(0) { }

大容量添付送ったらPOSTが空になって他の入力値も消失っすよ(笑)
(この上"どの画面に飛ぶか"も$_POSTに持たせる処理がありフォームに戻すことすらできない)
あーあ、未熟エンジニアの辛いとこね。これ。

---

## アップロード処理にまつわるサイズ制限
人間、食べられる量に限界があるように
Webサーバも想定した程よいサイズのリクエストしか食べることができない。
よほど専用に設計されたサービスでもない限り
100GBの添付が入ったPOSTデータ投げつけられて正常に処理できるものは少ないだろう。

このため、Webサーバ側で「明らかに胃袋のサイズを超えるリクエストは突き返してしまおう」と設定がされることがある

### Apache
デフォルトでは特に制限無くpostデータを投げることができるApacheだが
body長を制限することができるディレクティブが存在する
https://httpd.apache.org/docs/2.2/ja/mod/core.html#limitrequestbody

このディレクティブが設定されている場合、Webサーバはbody長とこのディレクティブの挿入値を比較する
もし流れて来たリクエストが大きすぎる場合は、アプリケーションに引き渡すことなく413(Request Entity Too Large)画面へ弾いてしまうことで、大量に処理時間を要し、サービスに負荷をかけることを防ぐことができる
<img width="883" alt="スクリーンショット 2019-02-03 17.20.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/0037aab4-8753-6d39-bbc1-eeae63c86308.png">


### Nginx
Apacheに対し、こちらはデフォルト値があり、「1MB」とかなりタイトな制限が適用されている。
https://qiita.com/n-oshiro/items/4816ad71b90a9967fa18
こちらもApacheと同様、リクエスト到着時点でサイズ長の比較を行い
リクエストが大きすぎた場合は、413画面に遷移させアプリケーションに処理を引き渡さず棄却する
<img width="537" alt="スクリーンショット 2019-02-03 17.23.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/21fb0b56-279b-4547-adef-bd2b9fd0f94b.png">




### php.iniが持つサイズ制限
Webサーバの制限とは別に、php.iniもまた以下のようなサイズ制限のプロパティを持っている

- post_max_size

```
post_max_size integer
POSTデータに許可される最大サイズを設定します。この設定は、ファ イルアップロードにも影響します。
大きなファイルをアップロード するには、この値を upload_max_filesize より大きく設定する必要があります。 
一般的に memory_limit は、 post_max_sizeよりも大きく する必要があります。 
integerを使用する際、 その値はバイト単位で測られます。 
この FAQ に記載された 短縮表記を使用することも可能です。 
POSTデータの大きさが、post_max_sizeより大きい場合、 $_POST と $_FILES superglobals は
空になります。この事象は、いくつかの方法で検出することができます。 
例えば、$_GET 変数をデータを <form action="edit.php?processed=1">のように 
処理するスクリプトに渡し、 $_GET['processed'] が設定されているかどうかを 確認する方法があります。
```

- upload_max_filesize

```
upload_max_filesize integer
アップロードされるファイルの最大サイズ。
```
(http://php.net/manual/ja/ini.core.php　 より抜粋)

まず、冒頭でも触れ、php.net中のpost_max_sizeにも記載のあった

```
POSTデータの大きさが、post_max_sizeより大きい場合、 $_POST と $_FILES superglobals は
空になります。この事象は、いくつかの方法で検出することができます。 
```

の文言。
この動きを実際に見てみよう。
ファイルをアップロードする為だけに生まれた適当なHTMLを作る

```html:test.html
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>サンプルフォーム</title>
</head>
<body>
  <form method="post" action="res.php" enctype="multipart/form-data">
    ファイル:<input type="file" name="up_file"><br>
    テキスト1:<input type=“text” name =“text1“><br/>
    テキスト2:<input type=“text” name =“text2“><br/>
    <input type="submit" value="upload">
  </form>
</body>
</html>


```

ここで流したpostデータをこのPHPで受ける

```php:res.php
<?php
  var_dump($_POST);
  var_dump($_FILES);
?>
```

仮に、2つのパラメータの制限が下記の通りだったとして

```
$ php -i | grep 'post_max_size'
post_max_size => 8M => 8M
$ php -i | grep 'upload_max_filesize'
upload_max_filesize => 2M => 2M
```

### 両方の制限内に収まっていた場合

<img width="228" alt="スクリーンショット 2019-02-03 15.40.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/3cbf7046-4ca0-227c-283b-d86b93647701.png">


```
array(2) { ["“text1“"]=> string(4) "hoge" ["“text2“"]=> string(4) "huga" } 
array(1) { ["up_file"]=> array(5) { ["name"]=> string(12) "test.txt.zip"
 ["type"]=> string(15) "application/zip" ["tmp_name"]=> string(26) 
"/private/var/tmp/php62WqN2" ["error"]=> int(0) ["size"]=> int(142) } }
```

当然ながら、パラメータが出現する。

では、制限を超過させてみた場合はどうだろう

<img width="267" alt="スクリーンショット 2019-02-03 15.49.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/7964471b-b105-0ac4-0496-08c94dfead19.png">

```
array(0) { } array(0) { }
```

なんということでしょう。
匠の技によって、本当に他のパラメータもろともPOSTデータが死滅してしまいました。

翻ってupload_max_filesize。
こちらは超過した場合の記述がないが、一体どうなるのだろうか？
upload_max_filesize <= ファイルサイズ <= post_max_size
となるファイルを添付してsubmitしてみよう。

```
array(2) { ["“text1“"]=> string(4) "hoge" ["“text2“"]=> string(4) "huga" } 
array(1) { ["up_file"]=> array(5) { ["name"]=> string(15) "Medium.zip" 
["type"]=> string(0) "" ["tmp_name"]=> string(0) "" ["error"]=> int(1) ["size"]
=>int(0) } }
```

一見正常な場合と変哲がないように見える。
が、よく見ると

```
["type"]=> string(0) ""
["error"]=> int(1)
["size"]=>int(0)
```
と、挿入されるパラメータに変化が見られる。
要はサイズが大きいのでエラーが発生し、正しくパラメータが入りませんでしたよ。ということだ。
挿入されているエラーコードは、いくつかパターンがある
http://php.net/manual/ja/features.file-upload.errors.php

成功は0で、この「1」は

```
UPLOAD_ERR_INI_SIZE
値: 1; アップロードされたファイルは、php.ini の upload_max_filesize ディレクティブの値を超えています。
```

という意味合いになる。

## 結局どうエラー処理すればいいんじゃ
実装前提や要件によるところが大きい為、あくまで一例として提示
でも

### こういう制限は開発者ツールで簡単に騙せるからやるなよ！絶対だぞ！

```html
<input type="hidden" name="MAX_FILE_SIZE" value="1000">
```

- オリジナル
<img width="1440" alt="スクリーンショット 2019-02-03 16.12.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/13ef833c-4041-c96a-d18b-530a85b6c53e.png">

- 改竄後
<img width="1440" alt="スクリーンショット 2019-02-03 16.13.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/9ff57590-a6f3-cbdd-169d-57382e250f6c.png">

- 簡単に欺けました(本来はエラーコード2が挿入される)
<img width="1440" alt="スクリーンショット 2019-02-03 16.13.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/265261/46edfeb0-4421-dc9d-b441-a0c9e275f729.png">

### upload_max_filesizeを超過するが、post_max_sizeには収まる場合
FILES変数に詳細が記載されるので、errorの値によって処理をスイッチさせるようにしよう

### post_max_sizeを超過する場合
そもそもFILESどころかPOSTすら死滅しているので、できるだけ前段で例外処理を行う必要がある。
FILES変数からサイズを受け取ることはできないが、SERVER変数にパケット長が受け渡されるので
ここをpost_max_sizeで比べるのが基本的な方法になるだろう

```php
<?php
  if ($_SERVER['CONTENT_LENGTH'] >= return_bytes(ini_get('post_max_size'))) {
    print("over!");
  } else {
    print("not over");
  }

  function return_bytes($val) {
    $val = trim($val);
    $last = strtolower($val[strlen($val)-1]);
    switch($last) {
        case 'g':
            $val *= 1024;
        case 'm':
            $val *= 1024;
        case 'k':
            $val *= 1024;
    }
    return $val;
}
?>
```

別途エラー処理を行っていて、入力値が全て入れられていないと
確認画面に飛べない。といったような実装がある場合単に

```php
if (empty($_POST)) {
...
}
```
という検知を行ってもいいかもしれない。

## 戒め
(初学者向けの教材等を除いて)自身の相対しているプロダクトと何もかも同じ背景・前提を持って
何かのエラーに悩んでいる人間はほぼ存在しない。
「あれ？$_FILESだけでいけるんじゃね？」みたいな時こそ一歩下がろう
何かの要件を見積もる際は、自身の思いつく範囲の設定・プロパティはもちろん
その関連事項もきちんと調べ上げよう
プロジェクトが走って死ぬのは自分だ

## リスペクト
端的ながら流れが掴みやすい記事でした。ありがとうございます。

ファイルのPOSTに立ちはだかる壁（PHP編）
https://qiita.com/jkr_2255/items/188200cafb869ca9c622