PHPのセキュリティ対策 続き 入力値検証

こちらの記事は以下の書籍を参考に執筆しました


[体系的に学ぶ 安全なWebアプリケーションの作り方 第2版［リフロー版］　脆弱性が生まれる原理と対策の実践](https://www.amazon.co.jp/%E4%BD%93%E7%B3%BB%E7%9A%84%E3%81%AB%E5%AD%A6%E3%81%B6-%E5%AE%89%E5%85%A8%E3%81%AAWeb%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9-%E7%AC%AC2%E7%89%88%EF%BC%BB%E3%83%AA%E3%83%95%E3%83%AD%E3%83%BC%E7%89%88%EF%BC%BD-%E8%84%86%E5%BC%B1%E6%80%A7%E3%81%8C%E7%94%9F%E3%81%BE%E3%82%8C%E3%82%8B%E5%8E%9F%E7%90%86%E3%81%A8%E5%AF%BE%E7%AD%96%E3%81%AE%E5%AE%9F%E8%B7%B5-%E5%BE%B3%E4%B8%B8-%E6%B5%A9-ebook/dp/B07HHT54YR/ref=sr_1_2?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E4%BD%93%E7%B3%BB%E7%9A%84%E3%81%AB%E5%AD%A6%E3%81%B6%E3%81%82%E3%81%B7%E3%82%8A&qid=1595990262&s=digital-text&sr=1-2)


#入力値の検証
前回記事でのセキュリティ脅威以外の脅威を防止するためにも
入力値を最大限にふるいにかけておけば、受ける被害を最小限に抑えることができます。
複数に渡って対策を施す(**多重防御**)が基本です。

##検証の前提
検証対象は**クエリ情報全部**です。
**すべてのグローバル変数は信用できない**ということを前提に考える必要があります。

また、javascriptで入力値を検証すれば安全であるとは言えません。
改ざんが簡単な上に、ブラウザでjavascriptを無効にされてしまっては意味がありません。
javascriptでの検証は一時的なものと捉え、最終的な検証はサーバサイドで行います。

##入力フォームの検証処理

定期的な検証を行うためのサンプルクラスを用意します。

```php5:MyValidator.php
<?php
require_once 'DbManager.php';

class MyValidator {
  private $_errors;
  //0
  public function __construct(string $encoding = 'UTF-8') {
    $_errors = [];
    mb_internal_encoding($encoding);
    $this->checkEncoding($_GET);
    $this->checkEncoding($_POST);
    $this->checkEncoding($_COOKIE);
    $this->checkNull($_GET);
    $this->checkNull($_POST);
    $this->checkNull($_COOKIE);
  }
  //①
  private function checkEncoding(array $data) {
    foreach($data as $key => $value) {
      if (!mb_check_encoding($value)) {
        $this->_errors[] = "{$key}は不正な文字コードです。";
      }
    }
  }
  //②
  private function checkNull(array $data) {
    foreach($data as $key => $value) {
      if (preg_match('/\0/', $value)) {
        $this->_errors[] = "{$key}は不正な文字を含んでいます。";
      }
    }
  }
  //③
  public function requiredCheck(string $value, string $name) {
    if (trim($value) === '') {
      $this->_errors[] = "{$name}は必須入力です。";
    }
  }
  //④
  public function lengthCheck(string $value, string $name, int $len) {
    if (trim($value) !== '') {
      if (mb_strlen($value) > $len) {
        $this->_errors[] = "{$name}は{$len}文字以内で入力してください。";
      }
    }
  }
  //⑤
  public function intTypeCheck(string $value, string $name) {
    if (trim($value) !== '') {
      if (!ctype_digit($value)) {
        $this->_errors[] = "{$name}は数値で指定してください。";
      }
    }
  }
  //⑥
  public function rangeCheck(string $value, string $name, float $max, float $min) {
    if (trim($value) !== '') {
      if ($value > $max || $value < $min) {
        $this->_errors[] = "{$name}は{$min}～{$max}で指定してください。";
      }
    }
  }
  //⑦
  public function dateTypeCheck(string $value, string $name) {
    if (trim($value) !== '') {
      $res = preg_split('|([/\-])|', $value);
      if (count($res) !== 3 || !@checkdate($res[1], $res[2], $res[0])) {
        $this->_errors[] = "{$name}は日付形式で入力してください。";
      }
    }
  }
  //⑧
  public function regexCheck(string $value, string $name, string $pattern) {
    if (trim($value) !== '') {
      if (!preg_match($pattern, $value)) {
        $this->_errors[] = "{$name}は正しい形式で入力してください。";
      }
    }
  }
  //⑨
  public function inArrayCheck(string $value, string $name, array $opts) {
    if (trim($value) !== '') {
      if (!in_array($value, $opts)) {
        $tmp = implode(',', $opts);
        $this->_errors[] = "{$name}は{$tmp}の中から選択してください。";
      }
    }
  }
  //⑩
  public function duplicateCheck(string $value, string $name, string $sql) {
    try {
      $db = getDb();
      $stt = $db->prepare($sql);
      $stt->bindValue(':value', $value);
      $stt->execute();
      if (($row = $stt->fetch()) !== false) {
        $this->_errors[] = "{$name}は重複しています。";
      }
    } catch(PDOException $e) {
      $this->_errors[] = $e->getMessage();
    }
  }
  //⑪
  public function __invoke() {
    if (count($this->_errors) > 0) {
      print '<ul style="color:Red">';
      foreach ($this->_errors as $err) {
        print "<li>{$err}</li>";
      }
      print '</ul>';
      die();
    }
  }
}
```

>出典：[体系的に学ぶ 安全なWebアプリケーションの作り方 第2版［リフロー版］　脆弱性が生まれる原理と対策の実践](https://www.amazon.co.jp/%E4%BD%93%E7%B3%BB%E7%9A%84%E3%81%AB%E5%AD%A6%E3%81%B6-%E5%AE%89%E5%85%A8%E3%81%AAWeb%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9-%E7%AC%AC2%E7%89%88%EF%BC%BB%E3%83%AA%E3%83%95%E3%83%AD%E3%83%BC%E7%89%88%EF%BC%BD-%E8%84%86%E5%BC%B1%E6%80%A7%E3%81%8C%E7%94%9F%E3%81%BE%E3%82%8C%E3%82%8B%E5%8E%9F%E7%90%86%E3%81%A8%E5%AF%BE%E7%AD%96%E3%81%AE%E5%AE%9F%E8%B7%B5-%E5%BE%B3%E4%B8%B8-%E6%B5%A9-ebook/dp/B07HHT54YR/ref=sr_1_2?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E4%BD%93%E7%B3%BB%E7%9A%84%E3%81%AB%E5%AD%A6%E3%81%B6%E3%81%82%E3%81%B7%E3%82%8A&qid=1595990262&s=digital-text&sr=1-2)


###0コンストラクタ  __construnct()
エラー情報を格納するする`$_error`を初期化し、文字エンコーディング検証、nullバイト検証をスーパーグローバル変数`$_POST`、`$_GET`、`$_COOKIE`に対して行います。

**文字エンコーディング検証、nullバイト検証は、絶対に行うべきなのでコンストラクタで無条件に実施します。**

###①文字エンコーディング検証 checkEncoding(array $data)
引数の値が指定した文字列であるかどうかを検証しています。

コンストラクタで文字コードを`mb_internal_encoding`関数により指定し、`mb_check_encoding`関数でチェックしています。

###②nullバイト検証 checkNull(array $data)
`preg_match`関数でnullバイト(\0)が含まれていないかどうかチェックします。

###③必須検証 requiredCheck(string $value, string $name)
値がセットされているかどうかを確認するには`trim`関数で前後の空白を除去し、その結果が空でないかをチェックします。

**`isset`関数は戻り値はboolですので、今回は使えません。**

###④文字列型検証 lengthCheck(string $value, string $name, int $len)
値がセットされており、文字数を比較してます。
文字の長さは`mb_strlen`関数を使用します。
`mb_strlen`関数はデフォルトの文字コード**(`mb_internal_encoding`関数)**を使用します。

対象の文字列と内部文字コードが異なっていると正しく文字列を取得できません。

###⑤整数型検証 intTypeCheck(string $value, string $name)
値がセットされており、`ctype_digit`関数により整数かどうかを検証しています。

>`is_int`関数は使えません。スーパーグローバル変数の値は文字列型であるため、型をチェックする`is_int`関数では常に`false`を返してしまいます。

###⑥数値範囲検証 rangeCheck(string $value, string $name, float $max, float $min)
数値の値が指定の範囲内であることを検証しています。
###⑦日付型検証 dateTypeCheck(string $value, string $name)
以下の日付形式に対応

- YYYY-MM-DD
- YYYY/MM/DD

渡された日付を`YYYY`,`DD`,`MM`の3つにわけ、それぞれを`checkdate`関数に渡します。
>checkdate ( int $month , int $day , int $year ) : bool



`checkdate`関数は文字列を渡されると警告を返すのでうざいので`@`(エラー制御演算子)で警告しないようにします。

###⑧正規表現パターン検証 regexCheck(string $value, string $name, string $pattern)
`preg_match`関数により、で与えられた正規表現パターンと引数`$_value`が合致するかをチェック。

メールアドレス、電話番号、URL等のチェックにも利用できます。

###⑨配列要素検証 inArrayCheck(string $value, string $name, array $opts)
与えられた配列`$opts`に引数`$value`が含まれているかをチェックします。
>in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) : bool

ラジオボタンや選択ボックスなど候補地が決められている入力チェックに利用します。

###⑩重複検証 duplicateCheck(string $value, string $name, string $sql)
DBへ接続してテーブル上同一データがあるかどうかをチェックします。
**取得した結果セットにフェッチできたなら、同一のデータが存在しているということです。**

引数`$sql`にはプレイスホルダ`:value`を含める必要があります。

###⑪エラー表示 __invoke()
オブジェクトを関数形式で呼び出した際にエラー表示をします。