安全なコードを書く為には、外部(データベース、OS、クライアントなど)へ出力するデータはデフォルトで全て無害化することが必要です。
無害化しなければならないデータは「形式」を持つデータ全てです。一文字でも意味がある文字がある文字列、特定の形式を持つデータ(データの位置に意味がある構造/形式)等全てです。
出力先のデータ形式には
等があります。Web環境では可変長テキストが多用されますが、無害化すべきデータは可変長テキストに限りません。構造化されてないテキストデータでも無害化が必要です。
文字エンコーディングのバリデーションは出力時にも行えますが、入力を受け付けた時点でバリデーションしないと問題を生む場合も少なくありません。入力バリデーションと出力時に出力を無害化する処理は独立したセキュリティ対策です。セキュアなコードを書く場合には、入力と出力の両方でセキュリティを意識したコードが必要です。
固定長/可変長のデータ(文字やデータ位置に意味があるデータ)、文字エンコーディングを持つデータは全て出力する前に無害化しなければなりません。
無害化の手段
これらの何れかの手段を出力するデータ全てに適用する。
出力先の無害化は”コンテクスト”によって異る。必ず適切な無害化方法を使用する。
例:SQLの出力先コンテクスト
パラメーターには更にコンテクストがある。全てのコンテクストに対して適切な無害化が必要
エスケープはコンテクストに合わせて行わなければならない。例えば、SQLインジェクション対策としてエスケープ/エスケープが必要ないAPIを利用しても、LIKEクエリや正規表現をサポートするDBMSの場合、LIKEクエリのエスケープ、正規表現のエスケープが必要となる。
データ形式がスカラーであるかどうか、で振るまいが変る出力先もあります。このようなAPIを持つ出力先にはデータ型のバリデーションも欠かせません。例:MongoDBインジェクション
データ形式をバリデーションしたり、出力データをエスケープするだけでは不十分な場合も少くありません。例えば、データベースクエリで取得するレコード数は最大値を制限(LIMIT句で取得可能なレコード数の値を制限)していないと、システムが応答不能になる問題が発生する場合があります。
出力データのサイズが大きすぎないようにする必要がある場合もあります。例えば、データベースのカラムサイズに制限がある場合(普通はある)はカラムサイズを超えないデータサイズを出力しないと、整合性のないデータがデータベースに保存される場合があります。例:PostgreSQLのvarchar型は大きすぎるデータでエラーとなるが、SQLite3のvarchar型は大きさをチェックしないで保存する。
このルールに非適合となるコードは多岐に渡る。非適合コード例は一部に過ぎない。
入力対策と出力対策は独立した対策である。出力時に無害化できるものは全て無害化する。
<?php $db = pg_connect('host=localhost'); if (!$db) { throw new Exception('Cannot connect to database'); } // There should be arbitrary length limit validation if (strspn(strlen($_GET['id']) > 100) { throw new Exception('Invalid integer format'); } // Validate $_GET['id'] contains only 0 to 9 chars if (strspn($_GET['id'], '0123456789') !== strlen($_GET['id'])) { throw new Exception('Invalid integer format'); } $id = $_GET['id']; // Usually there are many lines of codes and/or input/output // codes are stored in different files. // $id is used without escape/validation at output $sql = 'SELECT * FROM sample WHERE id = '.$id; $ret = pg_query($sql); if ($ret === FALSE) { throw new Exception('Query failed'); } ?>
この非適合コードの場合、$idが整数値としてバリデーションされていることに依存したコードになっている。出力対策は入力対策とは独立して行う。
一部のデータベースは整数などに文字リテラルをサポートしていない場合もあるが、ほとんどのデータベースはサポートしている。全てのパラメータに文字リテラルをサポートするデータベースの場合、すべて文字列としてエスケープする。
<?php $db = pg_connect('host=localhost'); if (!$db) { throw new Exception('Cannot connect to database'); } // There should be arbitrary length limit validation if (strspn(strlen($_GET['id']) > 100) { throw new Exception('Invalid integer format'); } // Validate $_GET['id'] contains only 0 to 9 chars if (strspn($_GET['id'], '0123456789') !== strlen($_GET['id'])) { throw new Exception('Invalid integer format'); } $id = $_GET['id']; // Usually there are many lines of codes and/or input/output // codes are stored in different files. // $id should be escaped/validated at output or use pg_query_params()/etc. $sql = 'SELECT * FROM sample WHERE id = '.pg_escape_literal($id); $ret = pg_query($sql); if ($ret === FALSE) { throw new Exception('Query failed'); } ?>
PostgreSQLの場合、pg_query_params()などのプリペアードクエリを利用しても良い。
識別子(テーブル名やカラム名)がパラメータの場合、PostgreSQLではpg_escape_identifier()が利用できる。識別子のエスケープ関数がないAPIを利用している場合、データベースの識別子エスケープ仕様に合わせて関数を作成し適用しなければななあい。LIKEクエリや正規表現を利用したクエリは、SQLコンテクストとは異る。必要な場合はLIKEクエリ、正規表現用のエスケープ関数を使ってエスケープしなければならない。
SQLite3のvarchar型は長さのバリデーションを行わない。
<?php $db = new PDO('sqlite:/tmp/mydb'); // Test table $db->query('CREATE TABLE test (str varchar(8));'); // Test data $str = 'aaaaaaaaaaaaaaaaaaaaa'; $db->query("INSERT INTO test (str) VALUES (".$db->quote($str).");"); ?>
sqlite3コマンドでmydbの内容を確認すると以下のような出力となる。
sqlite> select * from test; aaaaaaaaaaaaaaaaaaaaa
SQLite3のカラムは整数型のプライマリーキーを除き、全て可変長テキスト型である。このため、この非適合コードはカラムデータ型定義の制限を越えた文字列を保存する。
<?php $db = new PDO('sqlite:/tmp/mydb'); // Test table $db->query('CREATE TABLE test (str varchar(8));'); // Test data $str = 'aaaaaaaaaaaaaaaaaaaaa'; // PHP 5.5 or less requires encoding setting. if (!is_scalar($str) || !mb_check_encoding($str) || strlen($str) > 8) { throw new Exception('Invalid str'); } $db->query("INSERT INTO test (str) VALUES (".$db->quote($str).");"); ?>
この適合コードは適切なバリデーションを行う。CHECK制約やデータ型が利用できるデータベースの場合、データベース側のバリデーション機能を利用し、クエリエラーを検出する方法も選択できる。
<?php $id = $_GET['id']; if (!is_scalar($id) || strlen($id) > 8 || strspn($id, '1234567890') != strlen($id)) { throw new Exception('Invalid ID'); } echo 'ID: '. $id . '<br />'; ?>
HTMLコンテクストへの出力に対しHTML用のエスケープを行っていない。
<?php $id = $_GET['id']; if (!is_scalar($id) || strlen($id) > 8 || strspn($id, '1234567890') != strlen($id)) { throw new Exception('Invalid ID'); } // PHP 5.6以降のデフォルトの場合 echo 'ID: '. htmlspecialchars($id) . '<br />'; ?>
適合コードはコンテクストに対してエスケープ可能な全ての変数をコンテクスト用のエスケープを行う。エスケープしない変数は「エスケープできない変数」のみとする。HTMLコンテクストの場合、関数などで既に作られたHTML部品がエスケープできない変数である。
エスケープできない変数はプログラムによって自動処理できるようにするか、変数名によってエスケープができない変数であることが判るようにする。
エスケープできない変数を作成する場合、確実に安全な変数となるようにする。
文字エンコーディングのチェックは入力受付時に行う方が効率的である。全ての入力がバリデーションされている場合、出力時の文字エンコーディングバリデーションは省略しても構わない。
出力パラメータには無害化ができない物(HTML構文交じりのパラメータなど)もある。この場合、無害化できないパラメータを生成する場合に、全てのパラメータを無害化する。
出力対策と入力対策は独立した対策だが、入力対策だけで十分とするコードは間違いを起こしやすい。複雑なソフトウェアでは全てのパラメータが十分に安全なバリデーションを適用されているかを保証することも困難である。非常に単純なコード以外では出力時に全て無害化する。
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| IDS05-J | critical | likely | medium | P4 | L3 |