PHPセキュリティ対策:数値入力とバリデーション

(1/1)
プログラム利用者が不慣れで間違ったデータを入力したり、また、Web経由では悪意のある者が異常なデータを入力する可能性がある。こうしたデータがプログラムに渡されないよう、事前にデータのチェックを行う必要がある。
入力要件を満たしていなかったり、プログラムが想定していないデータかどうかをチェックすることを、ここではバリデーション(validation)と呼ぶことにする。
今回は、入力データが数値(整数および小数)である場合、PHPで入力バリデーションを行うプログラムを作ってみることにする。

(2022年6月27日)FastCGIで正常動作しない不具合を修正
(2022年5月18日)大幅改訂,pahooInputData.php分離,PHP8対応

目次

サンプル・プログラムの実行例

PHPセキュリティ対策:数値入力とバリデーション

サンプル・プログラム

圧縮ファイルの内容
getValidNumber.phpサンプル・プログラム本体。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。

解説:入力要件など

  40: //表示幅(ピクセル)
  41: define('WIDTH', 600);
  42: 
  43: //入力数値の種類 TRUE:整数,FALSE:小数
  44: define('CLASS_INT', FALSE);
  45: 
  46: //入力数値の最小値
  47: define('NUM_MIN', -9999);
  48: 
  49: //入力数値の最大値
  50: define('NUM_MAX', +9999);
  51: 
  52: /**

数値入力の要件として3つを用意した。各々は定数に定義する。
定数内 容
CLASS_INT整数か小数か
NUM_MIN最大値
NUM_MAX最小値
なお、データ入力に関わる関数群は別ファイル "pahooInputData.php" に分離しており、include_path が通ったディレクトリに配置すること。

解説:数値バリデーション

 241: /**
 242:  * 数値バリデーションを行う.
 243:  * 数値が整数か小数かを指定し,最小値と最大値を指定する.
 244:  * @参考URL https://www.pahoo.org/e-soul/webtech/phpsec/phpsec-09-01.shtm
 245:  * @param   mixed  $num       バリデーションしたい数値
 246:  * @param   string $errmsg    エラーメッセージを格納する
 247:  * @param   bool   $int       TRUE:整数/FALSE:小数(省略時:TRUE)
 248:  * @param   mixed  $min       最小値(省略時=-999999)
 249:  * @param   mixed  $max       最大値(省略時=+999999)
 250:  * @return  bool TRUE:成功/FALSE:失敗
 251: */
 252: function validNumber($num, &$errmsg, $int=TRUE, $min=-999999, $max=+999999) {
 253:     //整数のパターン:PHP7.4未満に合わせた
 254:     static $pat_int = array(
 255:         '/^[\+\-]?[1-9][0-9]*$/',
 256:         '/^[\+\-]?0$/',
 257:         '/^[\+\-]?0[xX].[0-9a-fA-F]+$/',
 258:         '/^[\+\-]?0[oO].[0-7]+$/',
 259:         '/^[\+\-]?0[bB].[01]+$/'
 260:     );
 261:     //小数のパターン:PHP7.4未満に合わせた
 262:     static $pat_float = array(
 263:         '/^[\+\-]?[0-9]*[\.][0-9]+$/',
 264:         '/^[\+\-]?[0-9]+[eE][\+\-]?[0-9]+$/',
 265:         '/^[\+\-]?[0-9]*[\.][0-9]+[eE][\+\-]?[0-9]+$/'
 266:     );
 267:     $res = TRUE;
 268:     $errsg = '';
 269: 
 270:     //引数チェック
 271:     if ($min > $max) {
 272:         $errmsg = "最小長({$min})が最大長({$max})より大きい";
 273:         $res = FALSE;
 274: 
 275:     //数値バリデーション
 276:     } else {
 277:         //値の存否
 278:         if ($num == '' || $num == NULL) {
 279:             $errmsg = '数値がありません';
 280:             $res = FALSE;
 281:         //整数かどうか
 282:         } else if ($int) {
 283:             $res = FALSE;
 284:             foreach ($pat_int as $pat) {
 285:                 if (preg_match($pat, $num> 0) {
 286:                     $res = TRUE;
 287:                     break;
 288:                 }
 289:             }
 290:             if (! $res) {
 291:                 $errmsg = '整数ではありません';
 292:             }
 293:         //小数かどうか
 294:         } else if (!$int) {
 295:             $res = FALSE;
 296:             foreach ($pat_int as $pat) {
 297:                 if (preg_match($pat, $num> 0) {
 298:                     $res = TRUE;
 299:                     break;
 300:                 }
 301:             }
 302:             foreach ($pat_float as $pat) {
 303:                 if (preg_match($pat, $num> 0) {
 304:                     $res = TRUE;
 305:                     break;
 306:                 }
 307:             }
 308:             if (! $res) {
 309:                 $errmsg = '小数ではありません';
 310:             }
 311:         }
 312: 
 313:         //最大・最小チェック
 314:         if ($res) {
 315:             if ($int) {
 316:                 $num = (int)$num;
 317:             } else {
 318:                 $num = (float)$num;
 319:             }
 320:             //最大値チェック
 321:             if ($num > $max) {
 322:                 $errmsg = "最大値({$max})より大きい";
 323:                 $res = FALSE;
 324:             //最小値チェック
 325:             } else if ($num < $min) {
 326:                 $errmsg = "最小値({$min})より小さい";
 327:                 $res = FALSE;
 328:             }
 329:         }
 330:     }
 331:     return $res;
 332: }

ユーザー関数 validNumber は、与えられた数字(文字列)$num が前述の入力要件を満たすかどうかバリデーションを行い、問題があった場合にはエラーメッセージ $errmag を戻す。

まず、引数 $min$max の大小関係に矛盾がないかどうかをチェックする。
次に数値バリデーションを行うが、下記の順序でバリデーションを行う。
  1. 値は存在するか(空文字ではないか)
  2. 入力要件が整数なら、整数にパターンマッチするか
  3. 入力要件が小数なら、小数にパターンマッチするか
  4. 最大値と最小値の範囲にあるか


整数のパターンマッチは正規表現で行うが、公式にあるPHPで取り扱い可能な整数表記を参考に、パターンを配列 $pat_int に用意した。
同様に、小数のパターンマッチも正規表現で行い、公式にあるPHPで取り扱い可能な小数表記を参考に、パターンを配列 $pat_float に用意した。入力要件が小数の時は、整数または小数のいずれかにマッチすれば要件を満たすことにした。

解説:バリデーション付き数値取得

 334: /**
 335:  * HTML FORMで指定したINPUTの内容を数値として取り出す(バリデーション付き)
 336:  * filter_input()関数および $argv を参照する.
 337:  * @参考URL https://www.pahoo.org/e-soul/webtech/php01/php25-01.shtm
 338:  * @param   string $key       パラメータ名(省略不可)
 339:  * @param   string $errmsg    エラーメッセージを格納する(省略不可)
 340:  * @param   mixed  $def       初期値(省略時:空文字)
 341:  * @param   bool   $int       TRUE:整数/FALSE:小数(省略時:TRUE)
 342:  * @param   mixed  $min       最小値(省略時=-999999)
 343:  * @param   mixed  $max       最大値(省略時=+999999)
 344:  * @return  string 入力値
 345: */
 346: function getValidNumber($key, &$errmsg, $def='', $int=TRUE, $min=-999999, $max=+999999) {
 347:     $str = (string)getParam($key, FALSE, $def); //URLパラメータを取り出す
 348:     $num = trim($str);                              //先頭・末尾の空白文字を除く
 349:     $num = htmlspecialchars($num);                  //XSS対策
 350:     $res = validNumber($num, $errmsg, $int, $min, $max);    //数値バリデーション
 351: 
 352:     return $str;
 353: }

ユーザー関数 getValidNumber は、HTML FORMに入力された数字を取り出し、バリデーションを実施する。
HTML FORMからデータを取り出すユーザー関数 getParam については、「PHPでGET/POSTでフォームから値を受け取る」を参照いただきたい。
次に、取り出したデータの先頭、末尾の空白文字を組み込み関数  trim  によって取り除く。
さらに、組み込み関数  htmlspecialchars  を使って特殊文字をエスケープしておく。これは後述するクロスサイトスクリプティング(XSS)対策のためである。
最後に、ユーザー関数 validNumber を使ってバリデーションを行う。
バリデーションの成否にかかわらず、入力データは、先頭・末尾の空白を取り除き、特殊文字をエスケープした状態で、戻り値とする。バリデーションで問題が発生した場合は $errmsg にエラーメッセージが入る仕組みである。

解説:メインプログラム

 194: // メイン・プログラム ======================================================
 195: //パラメータ
 196: $errmsg = '';
 197: $num = getValidNumber('num', $errmsg, '', CLASS_INT, NUM_MIN, NUM_MAX);
 198: 
 199: //リセット
 200: if (isButton('reset')) {
 201:     $num = '';
 202: }
 203: 
 204: //表示HTML作成
 205: $HtmlBody = makeCommonBody($num, $errmsg);
 206: 
 207: //表示
 208: echo $HtmlHeader;
 209: echo $HtmlBody;
 210: echo $HtmlFooter;

メインプログラムは簡素なものになった。
ユーザー関数 getValidNumber を使って、バリデーション付きで数値取り出しを行い、それを画面に表示する。

クロスサイトスクリプティング(XSS)

ユーザーから入力された文字列をそのまま(または少しだけ加工して)画面に  echo  や  print  で出力するプログラムを見かける。これは非常に危険である。
まず第一に、入力文字列の中にHTMLタグやJavaScriptが含まれていると、プログラマは意図していないにもかかわらず、そのまま実行されてしまう。
より危険なのが、入力文字列にスクリプトやCGIが書かれている場合である。プログラムの書き方によっては、そのまま実行されてしまうことがある。下手をすると、パスワードが漏れたり、データベースの内容が改竄されてしまうことがある。
こうした攻撃をクロスサイトスクリプティング(XSS)と呼んでいる。

処理系が扱える数値の最小値/最大値

最大値と最小値」で紹介したが、プログラム処理系で扱える数値には最小値/最大値がある。PHP処理系では、下表の定数に格納されている。
定数 内 容 備 考
PHP_INT_MIN 整数型の最小値  
PHP_INT_MAX 整数型の最大値  
PHP_FLOAT_MIN 浮動小数点型の最小値 PHP 7.2.0 以降で利用可能
PHP_FLOAT_MAX 浮動小数点型の最大値 PHP 7.2.0 以降で利用可能
PHPに限らないが、その処理系で扱える最小値/最大値は、実際にプログラムが稼働する処理系によって異なるものなので、プログラムによるバリデーションの範囲外である。仕様検討の時点で確定しておかなければならない。

バリデーションとベリフィケーション

この2つの用語は、業界によってニュアンスが異なるので、概念レベルで整理をしておく。

バリデーション(validation)とは、プロセスの妥当性を確認する作業である。*は、ここで用いた意味。
  • 書式の設定や文字の入力場所、保存されたデータの形式などが要求通りになっているかを確認する。(*)
  • 各開発工程の成果物が、当初の要求事項を満たしているかどうかを確認する。
  • それが有効であるか、価値があるかどうかを検証する。
  • 事件の立証や結果を法に則り認める。


ベリフィケーション(verification)とは、成果物が要求仕様通りに仕上がっているかを確認する作業である。
  • 異なるプロセスを経て出力されたデータが合致するかどうかを判定する。
  • 各々の開発工程の成果物が完成品として要求事項を満たしているかどうかを確認する。
  • 客観的証拠を提示することによって、規定の要求事項が満たされていることを確認する。

参考サイト

(この項おわり)
header