$_GET, $_POSTなどを受け取る際の処理

**【2021/10/15 追記】**
**この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので，過去の記事として参考程度にご覧ください。**

# 予備知識

PHPはフォームから送信された値などをコード実行開始に自動的に変数として使えるようにしてくれる非常に便利なプログラミング言語です．しかし，それをそのまま用いるとエラーが発生したり，脆弱性になってしまったりするケースがたくさんあります．使う前には適当なチェック処理が必要です．

## どういった変数が対象になるか

以下に挙げられた変数は，ユーザーが勝手に値や構造を書き換えたり，送信をそもそも行わずにアクセスしたりすることが可能な<ins>信用できない変数</ins>だと思ってください．例え，ラジオボタンで選択肢を限定していたり，隠し要素として埋め込んでいたりしたとしても，これに該当してしまいます．

### `$_GET`

- アクセスされたURLの `?` 以降のクエリーストリングに含まれる情報．
- `method` 属性の値が `get` であるフォームから送られた情報．

### `$_POST`

- `method` 属性の値が `post` であるフォームから送られた情報．

### `$_COOKIE`

- Webブラウザから送信されるクッキー．

### `$_REQUEST`

- `$_GET` `$_POST` `$_COOKIE` の内容が合成されて作られたもの．

なお， `$_SESSION` はサーバー側に保存される変数なので，このチェック対象にはなりません．チェックすべきはセッションの有効期限が切れていないか，つまり `$_SESSION` が空になってしまっていないかどうかです．

```php:セッションの有効期限切れをチェック
session_start();
if (!$_SESSION) {
    /* セッション有効期限切れに対応する処理 */
}
```

これらの特殊な変数に関する詳細は [「リクエストパラメータ・セッションに関するまとめ」](http://qiita.com/mpyw/items/7852213f478e8c5a2802) で詳しく解説しています．

## PHPのエラーの種類

PHPには主に以下のようなエラーがあります． (これで全てではありません)

### **E_PARSE** (別名: Parse Error, Syntax Error, 文法エラー)

コードが文法的に誤っているときに発生します．一切の処理を行いません．

### **E_ERROR** (別名: Fatal Error, 致命的なエラー)

処理を継続することが不可能になってしまった場合に発生します．発生したところで処理を停止します．これが発生するリスクを持つコードを書いてはいけません．

### **E_WARNING** (別名: Warning, 警告)

処理を継続することは可能ですが，想定外の問題が起こったときに発生します．これが発生するリスクを持つコードは可能な限り書かないでおくべきです．

### **E_NOTICE** (別名: Notice, 通知)

処理を継続することは可能ですが，想定内の問題が起こったときに発生します．これは手を抜いて無視する人と丁寧に対応する人に二分されますが，<ins>対応の仕方を知らないまま手抜きをするようなプログラマになってはならない</ins>と私は考えるので，ここでは徹底的に対応することにします．

## エラー発生による弊害

- 本番環境でエラーメッセージが表示されてしまうと，サーバーを攻撃しようと企む人にヒントを与えてしまう事になります．
- ページのデザインが崩れて不格好になります．

このため，エラーを表示させるのはデバッグ環境だけにすることが殆どです．但し，エラーを非表示にしたとしても…

- 処理速度が低下します． 
- php.iniの設定によってはエラーログが溜まります．
- php.iniの **error_reporting** の設定によっては表示されないエラーもありますが，処理速度の低下は防げません．
- **[エラー制御演算子](http://www.php.net/manual/ja/language.operators.errorcontrol.php) `@`** を使えば表示はされなくなりますが，処理速度の低下は防げません．またデバッグ環境で何らかのバグが発生してしまった状態でも，バグに関わるエラーメッセージが全て表示されなくなってしまいます．デバッグ作業がより難航することになるので，可能な限りこの演算子に頼ることは避けたほうが無難でしょう．

このようにさまざまなデメリットがあるので，可能な限りエラー発生を防ぐように書きましょう．

## 全てのエラーを表示

php.iniを以下のように編集すれば全てのエラーが表示されるようになります．

```ini:php.iniでの恒久的な設定
error_reporting = -1
display_errors = On
```

もしphp.iniの書き換えが出来ない場合，使用しているサーバーがApacheであれば以下のように **.htaccess** ファイルを設置することで対応できます．

```apache:.htaccessでの恒久的な設定
php_value error_reporting -1
php_flag display_errors On
```

それも不可能な場合や一時的に有効にしたい場合は，スクリプトの先頭で以下のコードを実行すればそれ以降は全てのエラーが表示されるようになります．但し **E_PARSE** はコードを読み取る段階で発生するエラーなので，これには対応することが出来ません．

```php
error_reporting(-1);
ini_set('display_errors', 'On');
```

## 比較演算子 `==` と `===` の比較

入門書などでは `==` が主体的に使われていることが多いですが，私は `===` に全て統一して使うことを推奨します．前者は **型の相互変換** を勝手に行います．PHP以外の言語出身者からすれば，これほど気持ち悪いものは無いでしょう．

- [PHP Manual - 比較演算子](http://us1.php.net/manual/ja/language.operators.comparison.php)

## [htmlspecialchars](http://www.php.net/manual/ja/function.htmlspecialchars.php) 関数によるエスケープ処理

HTML中で特殊な意味を持つ文字列は必ずエスケープしなければなりません．ユーザーから入力された値に対してこの処理を怠ると **[クロスサイトスクリプティング](http://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)** に関する脆弱性が発生してしまいます．

この処理を行うタイミングは<ins>表示する直前</ins>にしましょう．それ以外の場所でこの処理を行うべきではありません．例えばデータベースやファイルに格納するときにあらかじめエスケープしてしまうのは，ソフトウェア構成上の誤りだと言えます．

### `$string`

エスケープしたい文字列を渡します．

### `$flags`

フラグを指定します．省略した場合のデフォルト値は `ENT_COMPAT` となります．実際には `ENT_HTML401` との論理和となりますが，この関数の挙動には影響を及ぼさないのでここでは触れないことにします．詳しくは **[「htmlspecialchars関数やhtmlentities関数で使用されるフラグの検証」](http://qiita.com/mpyw/items/939c526dd5de6619b565)** を参照してください．

#### <font color="silver">ENT_COMPAT でエスケープされる文字</font>

`<` `>` `&` `"`

属性値を全てダブルクオーテーションで括っているのであればこれで問題はありません．但し，エスケープすれば安全だと思って安易に属性値として埋め込むのは避けましょう．例えば以下のケースではリンクをクリックしたときにJavaScriptが実行されてしまいます．

```php:不十分なXSS対策
// $_POST['url'] = "javascript:alert('XSS')"; とする
$url = htmlspecialchars($_POST['url']);
echo '<a href="' . $url . '">リンク</a>';
```

#### <font color="silver">ENT_QUOTES でエスケープされる文字</font>

`<` `>` `&` `'` `"`

これを指定しておくのが一番無難ですが，こちらも上記と同様に埋め込む対象となる属性によってはリスクが発生するので細心の注意を払ってください．

### `$encoding`

エンコーディングを指定します． PHP5.4以降とPHP5.3以前でデフォルト値が異なり，前者では `UTF-8` ，後者では `ISO-8859-1` となっています．前者であれば指定を省略することが可能ですが，バージョンによって挙動が異なってしまうのも問題なので，省略せずに指定することがマニュアルでも強く推奨されています．

<ins>ここまでは省略できない必須パラメータだと思ってください</ins>．

### `$double_encode`

`&amp;` `&lt;` `&gt;` 

のように既にエスケープされている文字をもう一度エスケープして

`&amp;amp;` `&amp;lt;` `&amp;gt;`

とするかどうかを論理値で指定します．デフォルトは `true` です．

### ラクに記述できる方法

表示するときに毎回

```php
<p><?php echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); ?></p>
```

と書くのは非常に煩わしいので，

```php
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
```

のような関数を作っておくと

```php
<p><?php echo h($str); ?></p>
```

として簡潔に書くことが出来ます．ここから末尾のセミコロンは省略して

```php
<p><?php echo h($str) ?></p>
```

とすることができ，更に **echo短縮構文** を使えば何と

```php
<p><?=h($str)?></p>
```

ここまで短く出来てしまいます．非常に便利な構文なのですが，これがデフォルトで使えるのは **PHP5.4以降** のみで，それより古いバージョンの場合はphp.iniで `short_open_tag` が有効化されている場合にしか使えません．もしphp.iniの書き換えが出来ない場合，使用しているサーバーがApacheであれば以下のように **.htaccess** ファイルを設置することで対応できます．

```apache:.htaccess
php_flag short_open_tag on
```

# チェック処理の概要

## [isset](http://www.php.net/manual/ja/function.isset.php)

### エラーの発生するケース

PHPでは，原則的に未定義の変数や配列インデックスの値を表示または取得しようとしたとき， **E_NOTICE** レベルのエラーが発生します．

- 変数が未定義  
  → **Notice:  Undefined variable**
- 配列インデックスが未定義  
  → **Notice:  Undefined index**

つまり，手抜きコーディングでよくある以下のようなチェック処理は不適当です．

```php
if ($_POST['email'] == '') {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

実際にフォームから送信してそのページに遷移した場合にはエラーは発生しませんが，<ins>直接そのページにアクセスした場合には</ins>**`$_POST['email']`**<ins>は未定義</ins>となるので，以下のエラーが発生してしまいます．

```txt:エラー
Notice:  Undefined index: email
```

なお，この手抜きのケースでは `===` を用いることは出来ません． 未定義の値を **E_NOTICE** レベルのエラーを発生しながら強引に取得しようとしたときその値は `NULL` と見なされるので， **型の相互変換** により `NULL` を `""` として判定させなければ正しい動作をすることが出来ないからです．

### エラーの発生を防ぐ方法

[isset](http://www.php.net/manual/ja/function.isset.php) は，変数が **未定義でない** かつ **NULLでない** ことをエラーを発生させずにチェックすることが出来ます．但し，外部からの入力を格納した変数が `NULL` を取るケースは存在しないので，このような使い方をする上では単に未定義でないかどうかのチェックを行うものとして考えていただいて構いません．

```php
if (!isset($_POST['email'])) {
    $errors[] = 'Eメールアドレスが送信されていません';
} elseif ($_POST['email'] === '') {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

「送信されていない」と「入力されていない」を区別する必要がない場合，以下のようにまとめてしまっても構いません．

```php
if (!isset($_POST['email']) || $_POST['email'] === '') {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

## [is_string](http://www.php.net/manual/ja/function.is-string.php) 関数<br />[is_array](http://www.php.net/manual/ja/function.is-array.php) 関数


最初に紹介した [isset](http://www.php.net/manual/ja/function.isset.php) によるチェックだけで自然な利用方法でも発生してしまう大半のエラーは防ぐことが出来ます．しかし，厳密に全てのエラーを潰すにはまだ不十分です．外部からの入力で受け取るものには **文字列** と **配列** の2種類があるからです．

例えば，こういうコードがあったとします．

```php:http://example.com/test.php
if (isset($_GET['var'])) {
    echo $_GET['var'];
    echo htmlspecialchars($_GET['var'], ENT_QUOTES, 'UTF-8');
    echo $items[$_GET['var']];
    echo 1 + $_GET['var'];
}
```

一見これで十分なように思えますが，エラーを完璧に潰すにはまだ不十分です．

`http://example.com/test.php?var[]=hoge`

正確にURLエンコードすれば

`http://example.com/test.php?var%5B%5D=hoge`

のようなクエリを受け取った時に， `$_GET['var']` は以下のような **配列** になってしまいます．

```php
[0 => 'hoge']
```

故にこの例では上から順に

```txt:エラー
Notice:  Array to string conversion
Warning:  htmlspecialchars() expects parameter 1 to be string, array given
Warning:  Illegal offset type
Fatal error:  Unsupported operand types
```

のようなエラーが発生してしまいます．

- 配列が **E_NOTICE** レベルのエラーを発生しながら文字列に強引に変換されたとき， `"Array"` という扱いになる．
- 入力値をそのままPHPの組み込み関数に渡している場合， **E_WARNING** レベルのエラーを発生してしまうケースが多い．
- 配列のオフセットにスカラー値または `NULL` 以外を指定したとき， **E_WARNING** レベルのエラーが発生する．
- 計算不可能なオペランド構成で `+` `-` などの演算子を使用したとき， **E_ERROR** レベルのエラーが発生する．

こういった事態を防ぐためには，厳密に **文字列であるかどうか** もしくは **配列でないかどうか** をチェックする処理が必要です．先ほど述べたように，可能性は **文字列** と **配列** の2種類しかないので，以下のどちらのパターンを採用しても構いません．意味が分かりやすいと思ったほうを選んでください．

```php:文字列であることをチェックしたい場合
is_string($var)
!is_array($var)
```

```php:配列であることをチェックしたい場合
!is_string($var)
is_array($var)
```

ここでは [is_string](http://www.php.net/manual/ja/function.is-string.php) 関数の返り値の否定を用います．

```php
if (!isset($_POST['email'])) {
    $errors[] = 'Eメールアドレスが送信されていません';
} elseif (!is_string($_POST['email'])) {
    $errors[] = 'Eメールアドレスが不正送信されました';
} elseif ($_POST['email'] === '') {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

「送信されていない」と「不正送信された」と「入力されていない」を区別する必要がない場合，以下のようにまとめてしまっても構いません．

```php
if (!isset($_POST['email']) || !is_string($_POST['email']) || $_POST['email'] === '') {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

配列の場合は各要素ごとのチェックも必要です．

```php
if (!isset($_POST['params']) || !is_array($_POST['params'])) {
    $errors[] = 'パラメータが送信されていません';
} else {
    foreach ($params as $key => $value) {
        if (!is_string($value)) {
            $errors[] = "パラメータ{$key}が不正です";
        }
    }
}
```

## [empty](http://www.php.net/manual/ja/function.empty.php)

[empty](http://www.php.net/manual/ja/function.empty.php) は，変数が **未定義である** または **型の相互変換により`false`と等しいと見なされる値である** ことをエラーを発生させずにチェックすることが出来ます．つまり以下のセットはそれぞれ同じ意味となります．

```php:空と見なす場合
!isset($var) || $var == false
!isset($var) || !$var
empty($var)
```

```php:空でないと見なす場合
isset($var) && $var == true
isset($var) && $var
!empty($var)
```

`""` は `false` と見なされる値の1つなので，最初の [isset](http://www.php.net/manual/ja/function.empty.php) だけを用いた例はこのように書くことも出来ます．

```php
if (empty($_POST['email'])) {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

2番目に紹介した [is_string](http://www.php.net/manual/ja/function.is-string.php) 関数によるチェックも織り交ぜるなら以下のようになります．

```php
if (empty($_POST['email']) || !is_string($_POST['email'])) {
    $errors[] = 'Eメールアドレスが入力されていません';
}
```

但し，`""` だけでなく `"0"` も `false` と見なされてしまい， **「入力したのに入力していないと言われた」** と利用者から文句が飛んでくるかもしれません．これぐらいの手抜きは場合によっては許されると思いますが，厳密な処理に拘る場合には避けるべきでしょう．著者はかなり気にします．

# フィルタ関数の活用

ここからは

1. スクリプトの先頭のほうで送信されてきた値をチェックする．有効なものであれば適当なローカル変数にその値を代入し， **未定義** もしくは **想定外の型** であれば `""` `NULL` `false` などを代入する．
2. そのローカル変数を用いて条件分岐を行い，適宜エラーに該当するかどうかをチェックする．

という処理フローを想定して「1.」の部分のコードを提示します．こちらの方が全体的な見通しが良くなり，入力フォームを再表示するときの利便性も向上するからです．

## 文字列のみを許可する

```php
if (!isset($_POST['name'])) {
    $name = null;
} elseif (!is_string($_POST['name'])) {
    $name = false;
} else {
    $name = $_POST['name'];
}
```

**[filter_input](http://www.php.net/manual/ja/function.filter-input.php)** 関数を利用すれば，これと等価な処理をもっと美しく書くことが出来ます．

```php
$name = filter_input(INPUT_POST, 'name');
```

文字列として送信されてきた場合のみ何か処理を行いたい場合は

```php
$name = filter_input(INPUT_POST, 'name');
if (is_string($name)) {
    /* 文字列として送信されてきた場合のみ実行したい処理 */
}
```

と書けば良いでしょう．

## 文字列を強制する

```php
if (!isset($_POST['name']) || !is_string($_POST['name'])) {
    $name = '';
} else {
    $name = $_POST['name'];
}
```

分岐パターンと代入先が1種類しかないので， **[三項演算子](http://www.php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.ternary)** を利用することが出来ます．

```php:&&を使う場合
$name = isset($_POST['name']) && is_string($_POST['name']) ? $_POST['name'] : '';
```

```php:||を使う場合
$name = !isset($_POST['name']) || !is_string($_POST['name']) ? '' : $_POST['name'];
```

[filter_input](http://www.php.net/manual/ja/function.filter-input.php) 関数と文字列へのキャストを利用すれば，これらと等価な処理をもっと美しく書くことが出来ます．

```php
$name = (string)filter_input(INPUT_POST, 'name');
```

## 1次元配列を強制する

```php
if (isset($_POST['items'])) {
    $items = $_POST['items'];
    if (!is_array($items)) {
        $items = [$items];
    }
    foreach ($items as $key => $value) {
        if (!is_string($value)) {
            unset($items[$key]);
        }
    }
} else {
    $items = [];
}
```

**配列へのキャスト** と **[array_filter](http://www.php.net/manual/ja/function.array-filter.php)** 関数を利用すれば，これと等価な処理をもっと美しく書くことが出来ます．

```php
$items = isset($_POST['items']) ? (array)$_POST['items'] : [];
$items = array_filter($items, 'is_string');
```

配列から文字列への強引なキャストでは **E_NOTICE** レベルのエラーが発生してしまいますが，<ins>文字列から配列へのキャストではエラーが発生しません</ins>．キー `0` に対する値を1つ持つ配列としてエラー無しにキャストされます．

## 指定したキーに限定して1次元配列を強制する

### テキストの配列を対象とする場合

```html
<form method="post" action="">
A: <input type="text" name="texts[a]"><br>
B: <input type="text" name="texts[b]"><br>
C: <input type="text" name="texts[c]"><br>
<input type="submit" value="送信">
</form>
```

テキストボックスは<ins>入力内容が無くても空文字列として送信されます</ins>が，不正なリクエストに備えるには結局冗長に書かざるを得ません．

```php
foreach (['a', 'b', 'c'] as $i) {
    $texts[$i] =
        isset($_POST['text'][$i]) && is_string($_POST['text'][$i]) ?
        $_POST['text'][$i] :
        ''
    ;
}
```

### チェックボックスの配列を対象とする場合

```html
<form method="post" action="">
<input type="checkbox" name="checks[a]" value="1">A<br>
<input type="checkbox" name="checks[b]" value="1">B<br>
<input type="checkbox" name="checks[c]" value="1">C<br>
<input type="submit" value="送信">
</form>
```

チェックボックスに関しては，<ins>チェックしていないものは送信されません</ins>．また，今回のように値を実際には必要とせず，単純にチェックされたかされなかったかのみを判定したい場合は

- チェックされたもの → `true`
- チェックされなかったもの → `false`

として存在させておいた方が何かと扱いやすいと思います．これを実現するには以下のようにします．

```php
foreach (['a', 'b', 'c'] as $i) {
    $checks[$i] = isset($_POST['checks'][$i]);
}
```

## 指定した値に限定して1次元配列を強制する

先ほどのチェックボックスの例に関して，今度はキーではなく値に着目し，想定されない値は含まないようにします．

```html
<form method="post" action="">
<input type="checkbox" name="checks[]" value="a">A<br>
<input type="checkbox" name="checks[]" value="b">B<br>
<input type="checkbox" name="checks[]" value="c">C<br>
<input type="submit" value="送信">
</form>
```

**[array_intersect](http://www.php.net/manual/ja/function.array-intersect.php)** 関数で値に注目したときの共通項を計算します． `0` から始まる数字添え字配列にした方が綺麗なので，必要に応じて最後に **[array_values](http://www.php.net/manual/ja/function.array-values.php)** 関数を通しておきます．

```php
$checks = isset($_POST['checks']) ? (array)$_POST['checks'] : [];
$checks = array_values(array_intersect($checks, ['a', 'b', 'c']));
```


# 更なるステップアップ

## [可変変数](http://www.php.net/manual/ja/language.variables.variable.php) の利用

[filter_input](http://www.php.net/manual/ja/function.filter-input.php) 関数を利用するとかなりコードを短くできますが，それでも項目数だけ繰り返しコールする必要がありました．

```php
$name = (string)filter_input(INPUT_POST, 'name');
$email = (string)filter_input(INPUT_POST, 'email');
$comment = (string)filter_input(INPUT_POST, 'comment');
```

これを **[可変変数](http://www.php.net/manual/ja/language.variables.variable.php)** という機能を利用することで，どれだけ項目が増えても1つ分の記述だけで済ませることが出来ます．但し，<ins>変数名 </ins>**`$v`**<ins> と一致する </ins>**`'v'`**<ins> が配列内に存在すると正しい処理が行えなくなる</ins>ので十分注意してください．

```php
foreach (['name', 'email', 'comment'] as $v) {
    $$v = (string)filter_input(INPUT_POST, $v);
}
```

もちろん，以下のように特定の配列やオブジェクトの中に格納することも可能です．お好みに応じて適当に使い分けてください．

```php:配列のインデックスを定義する
foreach (['name', 'email', 'comment'] as $v) {
    $p[$v] = (string)filter_input(INPUT_POST, $v);
}
```

```php:オブジェクトのプロパティを定義する
$p = new stdClass;
foreach (['name', 'email', 'comment'] as $v) {
    $p->$v = (string)filter_input(INPUT_POST, $v);
}
```

## 自分でフィルタリング用の関数を作る

複雑な配列をフォームから受け取る場合などには，上記の工夫だけでは間に合わないかもしれません．そういったときには自分でフィルタリング用の関数を作って処理をするのが適当です．以下に私が自作したとても便利な関数を紹介しておきます．

- [Qiita - filter_input_simple](http://qiita.com/mpyw/items/5a7242269f51dfabc973)
- [Qiita - filter_input_array_recursive](http://qiita.com/mpyw/items/c39b9ee695a5c2e74627)

## 具体的な値に関するバリデーションを行う

ここまでは

1. 未定義ではないか
2. 想定外の型ではないか

という処理のみに着目してきましたが，実際には

1. 未定義ではないか
2. 想定外の型ではないか
3. **誤った形式の値ではないか**

という処理になることがほとんどでしょう．EメールアドレスやURLなどのチェックがこれに該当します．詳しくは以下のまとめにて紹介しています．

- [Qiita - PHPで各種バリデーション](http://qiita.com/mpyw/items/346f1789ad0e1b969ebc)

## 推薦記事

- [Qiita - 【PHP超入門】HTTP（GET・POST）について](http://qiita.com/7968/items/4bf4d6f28284146c288f) by @7968