ストアドプロシージャの呼び出しでSQL文を動的に組み立てる場合
zip2addressの改修が終わったので、今度はzip2addressを呼び出す際のSQLインジェクション脆弱性について検討しましょう。先のストアドプロシージャ呼び出しはプレースホルダ「?」を使っていましたが、文字列連結によりパラメータを渡す実装もありえます。今度は言語をPHPとPDO(PHP Data Object)に変更した実装例を以下に示します。まずは正常系です。PDOにはプリペアードステートメント呼び出し用の専用メソッドはないため、通常のqueryメソッドで呼び出しています。
$dbh = new PDO(接続文字列, ユーザ名, バスワード);
$zipcode = '1420063';
$country = 'jp';
$sql = "CALL zip2address('$zipcode', '$country')";
$stmt = $dbh->query($sql);
// 以下略
これを実行すると、「東京都港区麻布十番」が返ります。
次に攻撃例です。$countryを以下のように指定します。
$country = "jp');DELETE FROM zip -- ";
この場合、呼び出されるSQL文は下記のとおりです。第2のSQL文としてDELETE文が追加されています。
CALL zip2address('1420063', 'jp');DELETE FROM zip -- ')
これを実行すると、zipテーブルの内容がすべて削除されてしまいます。SQLインジェクション攻撃の成功です。
このような攻撃を避けるためには、プレースホルダを使います。Javaの例は先ほど示しましたが、PHPとPDOの場合は以下のように記述します。これによりSQLインジェクション脆弱性は解消されています。
$stmt = $dbh->prepare("CALL zip2address(?, ?)");
$stmt->bindValue(1, $zipcode);
$stmt->bindValue(2, $country);
// ストアドプロシージャをコールします
$stmt->execute();
エスケープによる対策は難易度が高い
さて、ここでSQL Injection Prevention Cheat Sheetに話を戻しましょう。このドキュメントは第3の選択肢として、「Escaping All User Supplied Input(すべてのユーザ入力のエスケープ)」を挙げています。しかし、中身を読むと、手放しでエスケープによる方法を推奨しているわけではありません。該当箇所の冒頭には以下のように説明されています。
This second technique is to escape user input before putting it in a query. However, this methodology is frail compared to using parameterized queries and we cannot guarantee it will prevent all SQL Injection in all situations. This technique should only be used, with caution, to retrofit legacy code in a cost effective way. Applications built from scratch, or applications requiring low risk tolerance should be built or re-written using parameterized queries.
【参考訳】この第2の手法は、ユーザ入力をクエリ内に入れる前にエスケープすることです。しかし、この方法論はパラメータ化クエリ(訳注:プレースホルダを用いたSQL呼び出しのこと)を用いる場合と比較して弱く、我々はそれがすべての状況ですべてのSQLインジェクションを妨げると保証することができません。この手法は、既存のコードを費用対効果がよい方法で改修する場合に限り、用心深く用いられるべきです。スクラッチから構築するアプリケーションや、リスク許容度の低いアプリケーションは、パラメータ化クエリを用いて構築あるいは書き換えされるべきです。
私は上記見解に同意します。エスケープ手法は、既存のコードに脆弱性が発見された場合で、大幅な改修が難しい場合に限って使用すべきです。その場合は入力値検証など他の手法もあわせて用いることで安全性を高めることを推奨します。
結局のところSQLインジェクション対策の極意は、SQL文を動的に組み立てないこと
本稿ではストアドプロシージャのサンプルを用いながらSQLインジェクション脆弱性の例を示しました。脆弱性が混入している箇所はいずれもSQL文を動的に組み立てているところに問題がありました。従って、動的なSQL文組み立てを避け、変数(パラメータ)の指定をプレースホルダにより行うことを徹底すれば、SQLインジェクション脆弱性を解消することができます。それは、通常のSQL呼び出しの場合、ストアドプロシージャの定義の場合、ストアドプロシージャの呼び出しの場合のいずれにも共通する原則です。
現実のアプリケーションでは、SQL文の構造が動的に変化する場合も多いでしょう。その一例が、冒頭の例に示したように表名(の一部)を外部から指定するようなケースです。そのような場合は、外部由来の値を直接SQL文に混入させる実装をやめて、表名や列名として有効な値を配列など安全な場所に保持しておき、SQL文にはこの配列経由で指定することにすれば、外部由来の値を直接SQL文に混入してしまうことは避けられます。
以上を3か条の原則にまとめましょう。
- 原則としてSQL文を動的に組み立てない
- やむを得ずSQL文を動的に組み立てる場合は、列名やテーブル名などを補助的な配列などにより指定することで、外部由来の値をSQL文に混ぜない
- SQL文に指定する値はプレースホルダにより指定する
本稿がセキュアなウェブアプリケーション構築の一助になれば幸いです。
