DBレイヤーのオーバーライドの準備ができたとしても、リクエストとすべてのSQLを比較するのは、それなりにコストが高い手法であることを自覚する必要があります。
そこで、まずはDBレイヤーをオーバーライドする頻度を下げる努力をします。
ほとんどのリクエストには「攻撃となりえる」文字列は最初から含まれていないのですから、その時には元のDBレイヤーをそのまま使います。
あくまで、特定の文字列が含まれている時だけ、DBレイヤーをオーバーライドすることで、速度と安全性を両立できるでしょう。
もちろんその「特定の文字列」の判断こそが難しいのですが、こんなルールにしてみました。
/(information_schema|select|'|")/i
ここでは、引用符を壊す可能性のある'および"、そして、他のテーブルを参照する可能性のあるselectのみを検出しています。
unionは単独では意味を持たないのでここでは考慮しません。
また、コメント開始記号は誤認識が多い上(特に $_SERVER['HTTP_ACCEPT'] に*/*が含まれることが多い)、引用符を壊してからでないと攻撃としてもほぼ意味がないので、特定の文字列には含めません。
さて、SELECTや'や"を含んだ、「攻撃となりえる」リクエストを検出して、DBレイヤーのquery()メソッドをオーバーライドしたとしましょう。
次は、query()メソッドのなかで、SQL Injection攻撃の可能性があるリクエストと、実際に発行しようとしているSQLを比較します。
SQL Injection脆弱となるパターンには、大きく分けて2つあります。
(1) リクエストがクオーテーションの内側に収まるが、エスケープし忘れたもの
例)
SELECT ... FROM `table` WHERE `column`='(エスケープし忘れた文字列)'
(2) リクエストが最初からクオーテーションの外側にあるもの
例)
SELECT ... FROM `table` WHERE `integer_column`=(リクエスト)
SELECT ... FROM `table` WHERE ... ORDER BY (リクエスト)
ほぼ確実に防げるのは(1)のパターンです。
具体的には、リクエストの中で、シングルクオーテーションやダブルクオーテーションを含む文字列を記憶しておき、その文字列がそのままSQLに含まれるようなら、SQL Injectionだと判定してしまいます。というのも、まともなPOST文字列などであれば、クオーテーションの内側に収まっているはずなので、エスケープされていなければならないからです。
'や"を含む文字列をリクエストされた時に、たまたまリクエスト非依存のSQLと、偶然に一致してしまって、SQL Injection対策が効きすぎてしまう恐れはありますが、これはチェックすべきリクエスト文字列の最低字数を長くすることで可能性を下げることができます。さじ加減が難しいところではあるのですが、ギリギリ6文字未満の文字列は見逃す、くらいのバランスであれば、悪意あるSQL Injectionとしては有効な攻撃にならず、かつ、誤検出もほとんどでないでしょう。
このパターンでの最短と思われる有効な攻撃文字列はこうですから。
(もっと短いのがあったら教えてください)
逆に、(2)のミスをカバーしきるのは、かなり苦しいと言わざるを得ません。(2)のようなSQL Injection脆弱性がある時点で、そのtableのデータが読み書き自在とされるのは諦めるべきでしょう。
ただ、SQLとリクエストとの比較ロジックで頑張れば、他のテーブルへの波及はギリギリ防げると思います。
具体的には以下の2つの方法の併用です。
(A) SQL内にコメントがある時点でアウトとする
(B) SQLから文字列部分を取り除いたものの中に、SELECT等を含むリクエスト文字列が存在すればアウトとする
(A)はやりすぎな気もするのですが、現実にコメント付きのクエリを発行することはまずありませんし、怪しいリクエストが存在した時しかこのロジックは効かないので、ほぼ問題にならないだろう、と判断してます。
(B)がこのロジックの肝です。サブクエリにしてもUNIONにしても、基本はSELECTをどう埋め込むかです。そのSELECTを含んだリクエスト文字列が、クオーテーションの内側にない、という時点でアウトと判断できれば、他のテーブルに対するほとんどの攻撃を防げるでしょう。(もちろん、テーブル名一覧をinformation_schemaから取得する攻撃も)
このあたりの具体的な実装は、Protector-3.3 のProtectorMySQLDatabase.phpを確認してもらうのが良いと思います。