3回目となる本稿では、SQL識別子の取り扱いに関する問題を整理して、一般的な原則を導きたいと思います。
SQL文が動的に変化する場合のSQLインジェクション対策
「間違いだらけの…」で示したように、識別子エスケープが必要な局面でそれが洩れていると脆弱性の要因になることがありますが、それは外部から指定したデータにより、SQL文の構造が変化してしまい、アプリケーションの要件にないSQL呼び出しがなされてしまうからでした。しかし、「間違いだらけ…」の後半で示したように、識別子のエスケープだけではセキュリティ問題を防ぐことはできず、情報漏洩を招いてしまいました。外部から任意のSQL識別子を指定できることが問題という結論でした。
上記のように、アプリケーションでのSQL文組み立てにおいては、以下を考慮する必要があります。
- SQL文を構成するリテラル、識別子、予約語などを正しく構成する(条件A)
- テーブルや列、行などの権限があることを保証する(条件B)
- 文字列連結でSQL文を組み立てるな(原則1)
- パラメータはプレースホルダで指定せよ(原則2)
しかし、アプリケーションの要件によっては、条件によりSQL文が変化する(例:検索条件が変わる、ソート列が変わる)などの理由で上記原則に沿えない場合もあります。その場合には、どうすれば確実にSQLインジェクションを防ぐことができるか、ということが一連の議論のテーマです。
識別子やリテラルのエスケープ処理はSQL文を変化させないことが目的
SQLの識別子やリテラルのエスケープ処理は、SQL文を正しく構成するために必要なSQLの基本文法ですが、SQLインジェクション対策としてエスケープ処理が必要になる場合がある理由は、エスケープ処理を怠ると、SQL文の構造が変わってしまう場合があるからです。SQLリテラルのエスケープもれの例
上記に対して、$author = "'; delete from books#" とすると、以下のSQL文ができあがります。$sql = "SELECT * FROM books WHERE author='$author'";
第2のSQL文が追加されるという形で、SQL文の構造が変化しています。SELECT * FROM books WHERE author=''; delete from books#'
一方、SQL識別子のエスケープ漏れの例(MySQLを想定)
上記に対して、$booktable = "books`; delete from `books"とすると、以下のSQL文ができあがります。$sql = "SELECT * FROM `$booktable`"
やはり、第2のSQL文が追加されるという形で、SQL文の構造が変化しています。SELECT * FROM `books`; delete from `books`
文字列リテラルや識別子のエスケープ処理を正しく行うと、SQL文の構造を変化させることはできなくなります。しかし、識別子に関しては、エスケープ処理だけでは条件Aは満たせますが、条件Bは通常満たせません。
SQL識別子に関しては「もっと良い方法」がある
そもそも、SQL識別子をエスケープ処理しなければならない局面は以下であると考えられます。- データベース管理ツールを作成していて、識別子はユーザ入力である(局面1)
- アプリケーション内でテーブル名や列名をジェネレートしており、これらを構成する文字として引用符が使われる可能性がある(局面2)
局面2についてはですねぇ…「わざわざ、そんなややこしいことするな!」と言いたいですね。引用符どころか、識別子を構成する文字は英数字とアンダースコアに限定すればよいでしょう。加えて、識別子がSQLの予約語と衝突しないように工夫すれば、識別子をクォートする必要もありません。「予約語と衝突しない工夫」とは、例えば接頭辞(prefix)をつけることです。例えば、WordPressで用いるテーブルは標準でwp_という接頭辞がつきますが、wp_で始まるSQL予約語はないため、識別子が予約語と衝突しないことを保証できます。
識別子が正しく構成されることを保証する方法1: 入力値検証
とはいえ、識別子を構成する文字を英数字とアンダースコアに限定するだけでは、外部由来の文字列で組み立てた識別子が正しく構成されていることを保証できません。そのためのアプローチの一つとして、入力値検証による方法が考えられます。ここでは「入力値検証」という用語をプログラムの入り口で行うバリデーション(フォームバリデーション)という意味で用います。最初からこのチェックを入れておけば、識別子のクォートもエスケープも必要なく、利用者がアクセスできるテーブルも正しく制限できるのですが、この方法を紹介しなかったのは、理由があります。\Atb[1-4]\z
「入力値検証」によるSQLインジェクション対策の課題
前述のように、入力値検証で、テーブル名がtb1、tb2、tb3、tb4のいずれかであることを検証すると、SQLインジェクションや権限外のテーブルへのアクセスは防げますが、入力値検証のみに頼った対策の場合、以下の課題があります。- 入力値検証とSQL文組み立てはソースコード上の場所が離れている場合が多く、確認がしにくい
- 入力値検証と、組み立て後のSQL文の妥当性の関係が自明でない場合、仕様や実装の不備により脆弱性となりやすい
- 入力値検証とSQL組み立ては担当者が異なる可能性があり、仕様理解に差異があると、脆弱性混入の原因になる
- SQL文の妥当性は、SQL文組み立ての箇所で担保されるべきである
識別子が正しく構成されることを保証する方法2: 外部由来の値をSQL文に混ぜない
識別子が正しく構成されることを保証する(その結果SQLインジェクション脆弱性を防ぐ)方法の第2は、外部由来の値を直接SQL文中に混ぜないというものです。具体的には、(1)表名・列名は配列やハッシュに保持する、(2)if文やswitch文でハードコーディングする、(3)データベース上に辞書として保持するなどの実装が考えられますが、他の条件も含めて決めればよいでしょう。表名や列名の候補が非常に多く、配列やハードコーディングで保持できない場合は、少し妥協して以下のような実装でもいいでしょう(推奨という程ではありません)。
ただし、%d書式による数値化があるので安全なのであって、%s書式だと結局「外部由来の値をSQL文に混ぜている」ことになるので駄目です。$ident = sprintf("tb%03d", $n);
上記の方法でSQL文中の識別子を生成することにより、入力値検証にバグやもれがあっても、最悪SQLインジェクションは防げることになり、安全性が高い設計と言えます。さらに、条件Bとして説明した「権限のない表や列へのアクセスを防ぐ」こともあわせて実装されます(*1)。
なお、本稿では詳しく説明しませんが、SQL文中の予約語を指定する場合についても、同様の方法で安全に扱うことが可能です。
*1 ただし、複雑な認可処理はこれだけでは実現できないので、アプリケーションロジックによる認可の仕組みが必要です。
※ そもそも、表名や列名などの識別子は実装上の都合で決まる(決めて良い)ものであり、その名前を外部に露出することは好ましくない考えます。
蛇足: SQL識別子エスケープの困難性
現実問題として、SQLの識別子を正しくエスケープすることは難しい場合があります。一般に、SQLの(識別子ではなく)文字列リテラルのエスケープには、手作りのエスケープ関数を使わず、専用のAPIを呼び出すことが推奨されます(更に言えばプレースホルダを推奨しますが…)が、識別子のエスケープ用APIは、PHPのPostgres関数にはある(pg_escape_identifier関数。PHP5.4.4以降)ものの、他の言語やデータベースではあまり見かけません。ということは、識別子のエスケープには手作りのエスケープ関数を使わなければならない可能性が高いわけですが、「間違いだらけの…」で指摘したような考慮点が多いため、その意味でもできるだけ避けるべきでしょう。
まとめ
SQLの識別子の扱いについて検討しました。- SQLの識別子に使う文字は、英数字とアンダースコアに限定する
- 外部からの値から識別子を生成する場合は、表名の配列などを使用することにより、外部由来の値を識別子に(ひいてはSQL文に)混ぜないこと
- SQL予約語についても同様に扱う
- SQL文に指定する値はプレースホルダを用いる
- 文字列連結でSQL文を組み立てるな
- パラメータはプレースホルダで指定せよ
0 件のコメント:
コメントを投稿