こちらの記事は以下の書籍を参考に執筆しました
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版[リフロー版] 脆弱性が生まれる原理と対策の実践
入力値の検証
前回記事でのセキュリティ脅威以外の脅威を防止するためにも
入力値を最大限にふるいにかけておけば、受ける被害を最小限に抑えることができます。
複数に渡って対策を施す(多重防御)が基本です。
検証の前提
検証対象はクエリ情報全部です。
すべてのグローバル変数は信用できないということを前提に考える必要があります。
また、javascriptで入力値を検証すれば安全であるとは言えません。
改ざんが簡単な上に、ブラウザでjavascriptを無効にされてしまっては意味がありません。
javascriptでの検証は一時的なものと捉え、最終的な検証はサーバサイドで行います。
入力フォームの検証処理
定期的な検証を行うためのサンプルクラスを用意します。
<?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();
}
}
}
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()
オブジェクトを関数形式で呼び出した際にエラー表示をします。