カテゴリー
Computer Development PHP Security Programming Secure Coding

入力データのバリデーションを簡単に 〜 Validate for PHP 0.7.0

(Last Updated On: 2018/10/02)

基本的にWebアプリへの入力データは全てバリデーションする必要があります。具体的には以下のような構造でバリデーションが必要です。

Webアプリへの入力データの種類は大抵の場合、数百程度です。Validate for PHPはこれらの入力仕様を定義すれば、アプリケーション内で繰り返し同じ入力仕様をバリデーションできるように作られています。

Validate for PHPのスクリプト版をPre Alphaとしてリリースします。このブログのGitHubリポジトリのリンクは0.7.0ブランチに向いています。開発版はmasterブランチを参照してください。

基本的な使い方

  • 入力データ仕様($spec)を配列として定義する
  • validate()で入力データ($inputs)を入力データ仕様($spec)に適合しているかバリデーションする
  • validate()からバリデーション済みのデータが返される。アプリはこれを使う。

デフォルトでは仕様外の入力データにはInvalidArgumentExceptionを発生させます。フォームデータバリデーションで例外は困ります。例外を無効化することもできます。オプションの数はそれなり在ります。REFERENCE.mdを参照してください。

$inputsが配列の場合、バリデーション済みの要素は削除されます。

  • validate()に使った後の$inputsに「未検証」のデータのみ残る

この動作により、複数のバリデーションを行いデータを検証できます。つまり、全ての入力データをバリデーションできるように設計されています。

例1: 一つ一つデータをバリデーションする

MVCのコントローラーなどでバリデーションする場合(ユーザーとのやり取りが必要ない場合)のバリデーションに利用できます。

Webアプリの入力データにはHTTPヘッダー、GET、POSTがあり、さらに$_COOKIEの中身もチェックする必要があります一つ一つ指定するのは面倒です。後でまとめて一気にチェックする例を紹介します。

<?php
require_once __DIR__.'/../validate_func.php';
require_once __DIR__.'/../lib/basic_types.php'; // Defines $B (basic type) array

// Validate domain name
$domain = 'es-i.jp';
$domain = validate($ctx, $domain, $B['fqdn']);
// Validate record ID
$id = '1234';
$id = validate($ctx, $id, $B['uint32']);
// Check result
var_dump($domain, $id);

例2: フォームなどの入力をチェックする

<?php
require_once __DIR__.'/../validate_func.php';
require_once __DIR__.'/../lib/basic_types.php'; // Defines $B (basic type) array

$func_opts = VALIDATE_OPT_DISABLE_EXCEPTION;
// Validate domain name 
$B['fqdn'][VALIDATE_OPTIONS]['error_message'] = 'ドメイン名が不正です';
$domain = 'es-i.jp';
$domain = validate($ctx, $domain, $B['fqdn'], $func_opts);
// Validate record ID
$B['uint32'][VALIDATE_OPTIONS]['error_message'] = '整数の値が不正です';
$id = '1234';
$id = validate($ctx, $id, $B['uint32'], $func_opts);

if (validate_get_status($ctx) == false) {
    // Check $errors for interactive responses
    $errors = validate_get_user_errors($ctx);
    // Show useful $error here
}
//Check results
var_dump($domain, $id, $errors);

1つ注意点があります。validate_get_status()は最後のバリデーションのステータスを返します。同じ$ctx(未初期化だと自動初期化。最初のvalidate()呼び出しで初期化されている)を使っているので、2回目のvalidate()呼び出しの結果がvalidate_get_status()で分かります。

あまり良くない例なので、後で修正します。

$ctxが同じなのでvalidate()で発生したエラーやエラーメッセージは$ctxの中に保存されます。validate_get_user_errors($ctx)の配列の中身を見るとエラー発生状況が分かります。

一度にまとめてバリデーションするとvalidate_get_status()で全体のバリデーション状態を確認できます。

例3: まとめてバリデーションする

一つ一つ変数をバリデーションするのは非効率です。エントリポイント毎にまとめる、フォームごとにまとめないと管理が大変です。「まとめて管理する」はとでも重要です。Validate for PHPの場合、これが容易です。

複雑なバリデーションが必要な場合もあります。PHP関数のコールバックで「普通のPHPコード」でバリデーションルールが記述できます。APIの使い方を覚える必要もありません。

バリデーションルールは「PHPの配列」その物です。新しい文法/構文を覚える必要もありません。意味と種類を知るだけで使えます。ルールを作る為に変換したり、コードを実行する必要もないので効率も良いです。

<?php
// Simple "username" and "email" form validation example.
// "Validate" is suitable for "From Validations" also.
require_once __DIR__.'/../validate_func.php';
require_once __DIR__.'/../lib/basic_types.php'; // Defines $B (basic type) array

// In practice, you would define all inputs specifications at central repository.
// If your web app does not have strict client side validations, you will need
// "Input validation spec" AND "Business logic(Form) validation spec".

// If client JavaScript has validation
$username = [
    VALIDATE_STRING,        // "username" is string
    VALIDATE_STRING_ALNUM,  // "username" has only alphanumeric chars.
    ['min'=> 6, 'max'=> 40, // "username" can be 6 to 40 chars.
    'error_message'=>'Username is 6 to 40 chars. Alphanumeric char only.']
];

// "Validate" can be extend by callbacks.
$email = [
    VALIDATE_CALLBACK, // "email" is complex, so write PHP script for it.
    VALIDATE_CALLBACK_ALNUM, // Allow alpha numeric chars.
    ['min'=> 6, 'max'=> 256, 'ascii'=>'@._-', // Allow 6 to 256 chars and additional '@._-'
    'error_message'=>'Please enter valid email address. We only accepts address with DNS MX record.',
    'callback'=> function($ctx, &$result, $input) {     // Let's define rules by PHP function.
        $parts = explode('@', $input);
        if (count($parts) > 2) {         // Chars/min/max is already validated.
            $err =  "Only one '@' is allowed."; // This could be i18n function for multilingual sites.
            validate_error($ctx, $err);
            return false;
        }
        if (!dns_get_mx($parts[1], $mx)) {
            $err = "Sorry, we only allow hosts with MX record.";
            validate_error($ctx, $err);
            return false;
        }
        return true;
    }]
];

$spec = [ // Combine predefined parameter spec into one spec.
    VALIDATE_ARRAY,
    VALIDATE_FLAG_NONE,
    ['min'=>2, 'max'=>10], // Inputs must have 2 to 10 elements.
    [
        // Simply reuse predefined spec for parameters.
        "username" => $username,
        "email"    => $email,
        // You can validate $_GET/$_POST/$_COOKIE/$_SERVER/$_FILES at once by nesting.
    ]
];

$inputs = [
    'username' => 'yohgaki',
    'email' => 'yohgaki@ohgaki.net'
];

$func_opts = VALIDATE_OPT_DISABLE_EXCEPTION; // Disable exception, to check errors, etc.
$results = validate($ctx, $inputs, $spec, $func_opts); // Now, let's validate and done.

// Check results
var_dump(validate_get_status($ctx));        // $results is NULL when error. validate_get_status() can be used also.
var_dump($results, $inputs);                // $inputs contains unvalidated values.
var_dump(validate_get_user_errors($ctx));   // Get user errors.
var_dump(validate_get_system_errors($ctx)); // Get system errors.

例4: 余分なパラメーターをバリデーションする

$_GETやHTTPヘッダーにはアプリケーションでは直接は使っていない余計なパラメーターが含まれている事があります。validate()はバリデーション済みの値を戻り値として返します。余計なパラメーターがあっても、完全に無視することもできます。

しかし、OWASP TOP 10 A10:2017では上記のような動作をするアプリは脆弱なアプリである、としています。余分なパラメーターを含め、全てバリデーションします。様々なバリデーション方法があるのですが、先ずは基本パターンのみ紹介します。

<?php
require_once __DIR__.'/../validate_func.php';
require_once __DIR__.'/../lib/basic_types.php'; // Defines $B (basic type) array

$request_headers_orig = ['a'=>'abc', 'b'=>'456']; //apache_request_headers(); // Get request headers

// Check cookie and user agent. Allow undefined and extra headers.
$B['cookie'][VALIDATE_FLAGS]                 |= VALIDATE_FLAG_UNDEFINED; // Allow undefined(optional)
$B['user-agent'][VALIDATE_FLAGS]             |= VALIDATE_FLAG_UNDEFINED_TO_DEFAULT; // Allow undefined and set default
$B['user-agent'][VALIDATE_OPTIONS]['default'] = '';
$B['user-agent'][VALIDATE_OPTIONS]['min']     = 0; // Allow 0 length(empty)
$spec1 = [ // Explicit validations
    VALIDATE_ARRAY,
    VALIDATE_FLAG_NONE,
    ['min'=>2, 'max'=>20], // Inputs must have 2 to 20 elements.
    [
        'Cookie' => $B['cookie'],
        'User-Agent' => $B['user-agent'],
    ]
];

// validate() removes validated values from $request_headers_orig
$request_headers = validate($ctx, $request_headers_orig, $spec1);

// Check the rest of headers.
// Allow array 'header512' strings and ALNUM + '_' + '-' keys
$B['header512'][VALIDATE_FLAGS]   |= VALIDATE_FLAG_ARRAY | VALIDATE_FLAG_ARRAY_KEY_ALNUM;
$B['header512'][VALIDATE_OPTIONS]['min'] = 0; // Allow 0 length(empty) headers
$B['header512'][VALIDATE_OPTIONS]['amin'] = 0; // Allow 0 extra headers
$B['header512'][VALIDATE_OPTIONS]['amax'] = 20; // Allow 20 extra headers
$spec2 = $B['header512'];

// $request_headers has only validated values. No control chars nor multibyte chars.
$request_headers += validate($ctx, $request_headers_orig, $spec2);
// Check result
var_dump($request_headers, $request_headers_orig);

これはHTTPヘッダーのみの例でしたが、$GET、$_POST、$_COOKIE、$_FILESなどに対して同じような仕組みで全ての入力データをバリデーション可能になります。

まとめ

1つのリクエストの入力は多くても数十でしょう。アプリケーションへの入力種別は大抵の場合は数百程度まででしょう。

バリデーションルール仕様の定義は細かいので煩雑に見えるかも知れません。しかし、細かいルールの定義は1回&1箇所にまとめることができます。実際にコントローラーやフォームなどで使う際には、単純かつ解りやすい一次元配列になります。

Validate for PHPは配列でバリデーションルールを定義できるので、使いまわしが簡単です。

  1. アプリケーションへの各入力データの仕様を全て定義する配列を作る(この部分は煩雑に見える)
  2. 上記の仕様を使い、配列で各エントリポイント(MVCならコントローラー)の入力データ仕様を定義する(ここは単純な一次元配列)
  3. validate()でバリデーションする(基本、呼ぶだけ)

これでアプリケーションレベルの入力データバリデーションが完璧に行えます。

Validate for PHPはフォームデータのバリデーションや出力データのバリデーションにも使えます。

バリデーションはアプリケーション機能とは独立して追加できるので、足りていない所に追加すると良いでしょう。

を読めば基本的な使い方をマスターできると思います。

参考