SQLインジェクションは古くから知られている代表的なWebアプリケーションの脆弱性です。JavaScript関連の脆弱性に比べると対処が非常に簡単であるにも関わらず、まだまだ多くのアプリケーションがSQLインジェクションに脆弱です。SQLインジェクションに脆弱なアプリケーションはPHPアプリケーションに限った問題ではありません。Java、Perlなど、ほかの言語で作成されたWebアプリケーションにもまだまだ多くのSQLインジェクション脆弱性が残っていると考えられます。
重要なのは「正しく行えばSQLインジェクション対策は簡単」であることです。
SQLインジェクションの動作原理
SQLインジェクションの動作原理は単純で、解説するまでもないと思いますが、簡単に説明します。SQLインジェクションは、動的にSQL文を生成する際のパラメータに、プログラマが意図していない文字列を与えることにより、データを改ざんしたり盗むSQLクエリを実行させることです。
以下のコードはSQLインジェクションに脆弱な典型的な例です。
例:
$res = pq_query('SELECT * FROM product WHERE ID = '.$_GET['id']);
$_GET['id']に“ 0; DELETE FROM product” が送信されると、適切な権限があれば、productテーブルのすべてのデータが削除されます。UNIONクエリを利用するとデータベース内のすべてのデータを自由に取得可能となる場合もあります。
間違ったSQLインジェクション対策
SQLインジェクションがなくならない原因の一つに、間違ったSQLインジェクション対策で完全にSQLインジェクションを防ぐことが可能である、といった誤解があります。
エラーメッセージを表示しなければSQLインジェクションはできない
ORMを利用しているのでSQLインジェクションの心配はない
入力時にバリデーションしているのでSQLインジェクションの心配はない
すべての文字列はエスケープしているので心配はない
信用できるデータはエスケープなしでクエリに利用できる(信用できないデータだけエスケープすればよい)
これらすべては間違ったSQLインジェクション対策です。
エラーメッセージ抑制による対策
エラーメッセージを表示しなくても、SQLインジェクションによる攻撃は可能です。通常テーブル名、フィールド名は予測しやすい名前が付いています。テーブル名やフィールド名を予測しづらい名前にすることも可能ですが、簡単に防げるSQLインジェクションのために、著しくメンテナンス性を低下させるこのような手法を利用するのは間違ったSQLインジェクション対策といえます。エラーメッセージの抑制はSQLインジェクション対策として無意味でないですが、不十分極まりない対策です。
備考:いずれにせよエラーメッセージにテーブル名、フィールド名、変数名、スクリプトのパスなどシステムの内部構造が分かるようなメッセージを表示してはなりません。これはSQLインジェクション対策以前のセキュリティ上の問題です。
ORMに頼った対策
ORMを利用していても、SQL機能を使用せず、ORMの機能だけで効率的なアプリケーションが作れる訳ではありません。筆者も時々みかけるのですが、ORMだけ利用しているために酷く効率が悪いアプリケーションがあります。このようなアプリケーションはSQLインジェクションに脆弱でなくてもDoSに非常に脆弱です。実行効率の問題もあるので一般的なORMライブラリはSQL文やSQL文の一部を指定してクエリを実行できるようになっています。ORMを利用していてもSQLの基礎知識は必要です。
入力時のバリデーションに頼った対策
仮にHTMLフォームなどからの入力時に完全なバリデーションを試みていても、入力時のバリデーションに不具合があるケースは多くあります。動的にSQL文を作成するコードと入力をバリデーションするコードは別ファイルであることが多く、変数をSQL文の入力値として安全に利用できるか確認するには実行パスをたどって確認しなければなりません。このようなコードでは安全性の維持は容易ではありません。
すべての文字列をエスケープする対策
すべての文字列をエスケープしていてもSQLインジェクションが可能になってしまう場合がいくつかあります。これは不完全な入力バリデーションと組み合わされた場合に発生します。
以下のバリデーションには明らかな間違いがあります。整数を期待している箇所に文字列を許しています。
不完全なバリデーション(読者の皆さんで考えてください)
if (preg_match('/^[0-9]+$/', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
if (ereg('^[0-9]+$', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
ユーザ入力値をSQL文に利用するかしないかに関わらず、すべてのユーザ入力値は検証しなければなりません。上記の例は不完全なバリデーションによりSQLインジェクションが可能になる例ですが、後に解説する正しいSQLインジェクション対策を行っていればSQLインジェクション攻撃は行えません。
別の特殊なケースでは、数値型のコラムによりSQLインジェクションが可能となる場合もあります。データベースのアクセス抽象化ライブラリを利用すると複数のデータベースを同じように利用可能になりますが、データベースエンジンによっては通常のRDBMSでは思いもよらない動作をする場合があります。PHP5からデフォルトでバンドルされ利用可能になったデータベースエンジンのSQLiteのテーブルコラムには、整数、浮動小数点、文字列などのデータ型を指定できます。しかし、実際にはすべて文字列型として保存しています。SQLiteでは数値型のフィールドにセカンドオーダSQLインジェクション攻撃用のSQL文字列を保存させることができます。通常のRDBMSに経験を持つ方には数値型フィールドに文字列が保存されていることは予想できないかもしれません。
信用できないデータだけエスケープする対策
ここまでに紹介した不完全な対策から推測できるように、信用できないデータだけエスケープする対策ではSQLインジェクションを許してしまう可能性があることが分かります。動的にSQL文を生成する場合、すべてのデータは信用できないデータとして処理するべきです。
正しいSQLインジェクション対策
正しいSQLインジェクション対策には2種類の方法があります。
すべての変数をエスケープする対策
すべてのクエリをプリペアードクエリとして実行する対策
すべての変数をエスケープする対策
この方法はすべてのデータベースに利用できる対策です。文字列、整数などデータ型に関わらず変数すべてを文字列としてエスケープすることにより、SQLインジェクションを100%防ぐことが可能となります。例えば、PostgreSQLのSQL文を生成する場合、以下のようにすべてのパラメータを文字列して処理・生成します。
$sql = "UPDATE user SET name = '".pg_escape_string($_POST['name'])."', age = '".pg_escape_string($_POST['age'])."' WHERE id = '".pg_escape_string($_SESSION['USERID'])."';";
一部のドキュメントなどでは、addslashes関数を利用するなど、間違ったエスケープ方法を推奨しているケースがあるので注意しなければなりません。addslashes関数や置換関数(strtr, preg_repalce等)を使用した方法ではSQLインジェクションが可能になる場合があります。必ずデータベースインタフェースが提供しているエスケープ関数を利用するようにします(日本語などマルチバイト文字のエンコーディングの仕様が原因で、機械的な置換を行うとSQLインジェクションが可能となります。特にSJISは明示的にエンコーディングを意識してエスケープしないと、データベースサーバ側では意図したSQL文か攻撃用の文字列であるのかまったく区別ができません) 。
テーブル名、フィールド名を動的に決定している場合、エスケープは役に立ちません。このようなクエリを生成することはあまりないと思いますが、もし動的にテーブル名、フィールド名などを決定している場合、あらかじめ定義している文字列と一致しているか確認します。ユーザ入力値のテーブル名やフィールド名をチェックなしに直接クエリに使用すると簡単に不正なSQL文を実行可能です。
すべてのクエリをプリペアードクエリとして実行する対策
プリペアードクエリとは、実行するクエリをあらかじめパースし、パラメータを渡すだけでクエリの実行を行えるようにするDBMSの機能です。すべてのDBMSがプリペアードクエリをサポートしていないので、プリペアードクエリをサポートしているDBMSを利用している場合にのみ使用できる対策です。プリペアードクエリに渡されるパラメータはすべて値として処理されます。このため、プリペアードクエリのみでクエリを実行するとSQLインジェクションは不可能になります。
$res = pg_query_params($conn, 'UPDATE user SET name = $1, age = $2 WHERE id = $3', array($_POST['name'], $_POST['age'], $_SESSION['USERID']));
どちらかというとプリペアードクエリを利用したほうが安全性が高く(エスケープする方法ではエスケープ漏れのリスクが高い) 、後でコード監査を行う場合も容易に監査できます。プリペアードクエリが利用可能な場合はプリペアードクエリを利用するほうがよいでしょう。
しかし、プリペアードクエリだからといって安心できない場合もあります。データベースアクセス抽象化ライブラリを利用する場合、プリペアードクエリのようなインターフェースをサポートしていても、実際はプリペアードクエリでない場合があります(PDO、Zend Frameworkなど) 。このようなライブラリを利用している場合、ライブラリの不備や利用方法に誤りがあるとSQLインジェクションに脆弱となる場合があるので注意が必要です。
まとめ
SQLインジェクションはすべての変数をエスケープするか、すべてのクエリをプリペアードクエリとして実行すれば100%防げる脆弱性です。ソースコードからこの2種類の対策が完全に行われているかチェックするのは非常に容易です。XSS等のJavaScript関連の脆弱性に比べ、SQLインジェクション対策は単純かつ明快です。
不完全なバリデーションの答え
preg_match
if (preg_match('/^[0-9]+$/', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
は、$_GET['id']中の最初の1行目が数字だけで構成されているかチェックしています。2行目以降にどのような文字列が入っていても構いません。PostgreSQL/MySQLなどのDBMSもクエリ中の改行を許可しているので、SQLインジェクションが可能になります。正しいチェックは以下の通りです。
if (preg_match('/^[0-9]+$/D', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
ereg
if (ereg('^[0-9]+$', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
ereg関数はバイナリセーフな関数でないので、\0までしか評価対象になりません。ereg関数はユーザ入力のバリデーションには利用してはならない関数です。\0等の特殊文字を別の文字に置換してからチェックすればereg関数を利用できます。しかし、ereg関数はpreg関数やmb_ereg関数に比べ非常に遅く、わざわざ面倒な処理を遅い関数で行う必然性はありません。
おまけ:mb_eregを利用した場合
if (mb_ereg('^[0-9]+$', $_GET['id'])) {
$id = $_GET['id'];
}
$res = pq_query('SELECT * FROM product WHERE ID = '.$id);
PHPマニュアルにもオプションの解説などが記載されていないので、このケースは多少わかり辛いです。
mb_regexのデフォルトオプションは“ pr” が設定されています。“ p” オプションはmb_regexが利用しているoniguruma正規表現ライブラリの以下のオプションを有効にします。
ONIG_OPTION_SINGLELINE
‘^’ →‘ \A’ 、‘ $’ →‘ \z’ 、‘ \Z’ →‘ \z’
ONIG_OPTION_MULTILINE
‘.’ が改行にマッチする
\Aは文字列の先頭(行頭でないことに注意) 、\zは文字列の最後(行の最後でないことに注意)が設定されます。“ r” オプションはRubyスタイルの正規表現文法を利用する場合に設定するオプションです(参考:http://www.geocities.jp/kosako3/oniguruma/doc/RE.ja.txt ) 。
mb_eregを利用した場合はデフォルト状態で期待どおりに処理され、安全にクエリが実行できます。とはいっても、今は安全でも将来PHPのバグ等で安全に実行できない状態にならないとは言い切れません。エスケープ処理またはプリペアードクエリを利用するだけで高いの安全性を確保できるので、どちらかのSQLインジェクション対策を取るべきです。
以前、データベースシステムのライブラリを利用した関数、pg_escape_stringやmysql_real_escape_stringを利用していても、文字エンコーディングを利用したSQLインジェクション攻撃には脆弱であることが発見されました。しかし、PostgreSQLとMySQLのアクセスライブラリは迅速にこれらの脆弱性に対処しました。pg_escape_string関数やmysql_real_escape_string関数を利用していればPHPスクリプトを修正することなく脆弱性に対処することが可能でした。このことからもデータベースライブラリが提供するエスケープ関数を利用することの重要性を理解できると思います。