オープンソースの脆弱性対策に学ぶ ~DrupalのSQLインジェクション対策(SA-CORE-2015-003、CVE-2015-6659)~ 2/4
前回はDrupal ver.7.39で対策されたSQLインジェクションの脆弱性について、ソースコードの変更点をご紹介しました。
変更前のソースコードには、SQLコメントの位置に埋め込む値の中から「/* 」や「 */」といった部分文字列を見つけて除去する処理が記述されていました。
SQLインジェクションの脆弱性とは?
アプリケーションが動的にSQL文を組み立てる際の不備を利用して、想定しないSQL文を実行させることによりデータベースを不正に操作することができる脆弱性です。
外部から渡された値を使用してSQL文を組み立てる場合、アプリケーションは渡された値がSQLの構文に作用することのないよう、値を埋め込む位置に応じた対策を行う必要があります。
例えば以下のソースコードは、SQLインジェクションの脆弱性を示します。
$param = $_POST['param'];
$query = "SELECT * FROM TABLE1 WHERE STR_COLUMN1 = '$param'";
$result = exec_query($query);//exec_query関数はSQL文を発行します。
POSTパラメータ「param」に値「’」が渡された際の$queryの内容は、
SELECT * FROM TABLE1 WHERE STR_COLUMN1 = '''
となり、文字列引用符「’」の数が合わないためにSQLの構文エラーが発生します。
文字列引用符に使用される「’」が文字列の値に含まれるくらいでSQLの構文エラーが発生するのは困りますので、通常は「”」のようにエスケープする方法が文法上定義されています。
$queryの内容が
SELECT * FROM TABLE1 WHERE STR_COLUMN1 = ''''
であれば、文法上は問題なく実行可能となります。
このように文字列リテラルの位置に値を埋め込む場合、「’」や文法上特別な意味を持つ幾つかの記号文字をエスケープする必要があります。
どのような記号文字をエスケープすべきかは、データベース製品によって異なる場合があるため、製品のマニュアルを参照してください。
一方、数値など文字列以外のリテラルは文字列リテラルとは異なり、文法上定義されたエスケープが存在しません。
したがって、文字列以外のリテラルの位置に値を埋め込む場合、埋め込む直前に値の型や形式を厳密にチェックし、問題ないことを確認した上で埋め込みに使用します。
また、リテラルの位置に値を埋め込む代わりに、プレースホルダーを使用することで埋め込み値がSQL文に作用することを回避する方法があります。
例えば以下のソースコードは、SQLインジェクションに対して安全です。
$param = $_POST['param'];
$query = 'SELECT * FROM TABLE1 WHERE STR_COLUMN1 = ?';
$stmt = prepare_query($query);//prepare_query関数はSQL文を準備します。
$bindvar = array($param);
$result = exec_statement($stmt, $bindvar);//exec_statement関数は準備済みSQLを実行します。
プレースホルダーはSQL文のリテラルの位置に対して有効ですが、その他の位置では使用できません。
その他の位置とは、例えばテーブル名、カラム名といったオブジェクト識別子や予約キーワードなどのことです。
その他の位置
SELECT name, zip, addr1, addr2, addr3, tel, mail FROM addrbook WHERE id = ? AND status = 1 --その他の位置がほとんど
その他の位置に外部から渡された値を埋め込む必然性はあまりないように思いますが、もしそのように実装をしたいのならば、埋め込む直前に値の内容を厳密にチェックし、問題ないことを確認した上で埋め込みに使用します。
また、近年は文字列変数を使用してSQL文を組み立てるのではなく、ビルダー機能を提供してSQL文を組み立てるライブラリが充実してきています。
ビルダーライブラリの登場によって、文字列変数を使用してSQL文を組み立てず、ライブラリの責任においてSQLインジェクションを回避できるようになってきました。
実は、Drupalはアドオンモジュール開発者向けにビルダーライブラリを実装しています。
例えば以下のソースコードは、ライブラリがSQL文を安全に組み立てて実行します。(https://www.drupal.org/dynamic-queries より引用)
$query = db_select('users', 'u') ->condition('u.uid', 0, '<>') ->fields('u', array('uid', 'name', 'status', 'created', 'access')) ->range(0, 50);
組み立てられたSQL文
SELECT uid, name, status, created, access FROM {users} u WHERE uid <> 0 LIMIT 50 OFFSET 0