ちょっと硬派なコンピュータフリークのBlogです。

カスタム検索

2013-12-14

SQLインジェクション対策に正解はない

最近、SQLインジェクションのネタが盛り上がってるようだ。下記のTogetterまとめあたりが震源地だろうか。

「プリペアードクエリが基本だけど、動的に SQL を組み立てる場合もあるから、そういう場合に備えてエスケープも知っておいたほうがいいかも」 - Togetterまとめ

まとめを読んだ感想としては、「どちらの意見も間違ってはいない」というものだ。前提あるいは見方が異なるために、見解の相違が生じているだけのように思う。SQLインジェクションについては私も若干思うところがあるので意見を書いておこうと思う。

攻撃を防ぐのは難しい

SQLインジェクションをはじめとするセキュリティ対策が難しいのは、ひとつでも穴があると致命的なダメージを受け得るということだ。「どうやって効率よくコードを書くか」とか「コードのメンテナンス性を高めるにはどう書くべきか」みたいな議論とは全く質が異なる議論が必要になってくる。穴がひとつでもあるとアウトなのだから、「どうすれば完璧に穴を塞ぐことができるか」というのが最も重要なテーマとなる。

プリペアドステートメントだけでは不十分

以前、「SQLインジェクションとは何か?その正体とクラッキング対策。」というタイトルのエントリを書いた。このエントリでは「SQLインジェクションの最も確実で根本的な対策は、プリペアードステートメントを利用することである。」と書いたが、改めて考えなおすとプリペアドステートメント(プリペアドステートクエリ)だけでは対策としては不十分であると思う。と言ってもプリペアドステートメントそのものに問題があるわけではなく、プリペアドステートメントが使えないようなクエリがどうしても存在するからである。

大垣氏の「SQLのエスケープ」というエントリではIN句が例として挙げられている。IN句が何故プリペアドステートメントで表現するのに向いていないかというと、SQLではIN句の中の要素の数までがシンタックスの一部となっているからだ。つまり、IN句に含まれる要素数が異なると、シンタックス上異なる構造を持つSQL文となる。そのため、どうしても静的なSQLを用いるなら、要素数ごとにプリペアドステートメントの元になる静的な文字列(Preparable Statement)を準備しなければならない。事前に要素数が最大でいくつまでになり得るかが分かっていればそのようなそのような方法で乗り切ることも可能であるかも知れないが、そうでない場合や、多くの要素数を許容するような場合には、IN句の要素数の異なる文字列をプログラムで生成するといった対応が必要となる。要素数を引数に取り、それに応じたプレースホルダーを出力するだけであれば、SQLインジェクションが入り込む余地はないが、これは静的SQLであるとは言い難い。

柔軟な検索

IN句のような単純なものならばまだマシなのだが、世の中にはもっと厄介な例が存在する。いわゆる「柔軟な検索」というヤツだ。多くのウェブサイトには、「Advanced Search」だとか「こだわり検索」だとかいう名称で、ユーザーが欲しい情報を得るための柔軟な検索機能がついている。そのような検索機能では、どの条件を指定するかがユーザーに委ねられており、ユーザーが指定しなかった条件は検索結果に影響を与えないようになっている。柔軟な検索には例えば次のようなものがある。
このような入力した条件だけが使われる検索は、当然SQLの方も指定された条件だけをWHERE句に含めることになる。このようなケースでは、静的な文字列を用いてプリペアドステートメントを準備するのは絶望であると言える。なぜなら、柔軟な検索条件が多くなると、爆発的にSQLが取り得る文法のバリエーションが増えてしまうからだ。例えば検索条件がn個存在し、ユーザーがその中から任意の数の条件を指定することができる場合、取り得る組み合わせの数はなんと2^n(2のn乗)となる。(二項定理)もし検索条件が10個あれば1024ものバリエーションが存在することになる。20個なら100万個だ。これはもはや静的な文字列だけで対応できるような領域ではない。

そこで、我々は選択を迫られる。プリペアドステートメントの元になる文字列を動的に組み立てるか、プリペアドステートメントを用いずにクエリを動的に組み立てるか、である。

当然、前者のケースではパラメーターの部分に関してはSQLインジェクションが入り込む余地はないのでその分対応は楽になる。だが、当然ながら動的にクエリを組み立てるのだから隙がないわけではない。例えば、リテラルを誤ってパラメーター化せずに(プリペアドステートメントの元になる文字列に)含めてしまうというミスが起きることも考えられるし、識別子に不正なSQLを注入されてしまう恐れもある。例えばパラメーターをJSONでブラウザから送信し、サーバー側でそれをパースして検索条件を組み立てるというような場合を考えて見て欲しい。[{"key1":"val1"},{"key2":"val2"},...]という配列をリクエストで受け取り、WHERE key1 = ? AND key2 = ? ...というようなSQLへ変換してしまうとどうなるだろうか。真っ当なプログラマはこのような実装にはしないだろうが、世の中には驚くべき手法で常識が乗り越えられてしまう例は枚挙に暇がないので油断はならない。(この場合、識別子を予め決められた文字列だけを通すようにするか、もしくはエスケープすることにより、SQLインジェクションを防ぐことができる。)

後者の場合、つまり動的にクエリを組み立てる場合は、識別子とリテラルをそれぞれ適切にエスケープまたはバリデーションすれば良い。エスケープは漏れが怖いのであって、確実にエスケープすればSQLインジェクションは起こらない。「静的なSQLではSQLインジェクションは理論上起こらない」という主張が正しいのと同様に、「漏れ無くエスケープしていればSQLインジェクションは理論上起こらない」という主張もまた正しい。

メタデータ大増殖

柔軟な検索以外にも、プリペアドステートメントでは対応できないケースはある。例えば書籍「SQLアンチパターン」に載っているメタデータ大増殖だ。メタデータ(=識別子)の部分はプリペアドステートメントではパラメーター化できないので、適切にそれらをエスケープしてやる必要がある。

アンチパターンとなっているように、基本的には識別子にデータを含めるようなことはしてはならない。だが、書籍で紹介されるほどのアンチパターンだということは、裏を返せばこれはよくある間違いだということだ。

何故このような問題がよく起きるのか。

データとメタデータを常にキッチリと分けられると考えるのは早計だ。何故なら、データとメタデータの間には明確な境界線は存在しないからだ。データとメタデータは、我々がそれらを常識に基づいて区別(あるいは設計)しているだけであって、それらを明確に区別するための方式や法則といったものは存在しない。そのため、メタデータに情報が混入してしまうことは常に起こり得る事象だと考えておいたほうが良いだろう。特にデータベースの規模(≒テーブル数やカラム数)が大きくなればなるほど危険性は増すだろう。

SQLアンチパターンで紹介されている、EAVポリモーフィック関連といったものも、メタデータとデータが混合することによるアンチパターンである。

対策の組み合わせ

これまで見て来たように、プリペアドステートメントだけでは対応できないケースというのは意外と多い。プリペアドステートメントだけで対応できない場合に備え、やはりエスケープやバリデーションといった教育は必要ではないかと思う。

プリペアドステートメントはあくまでもツールであって、ただ仕様通りに仕事をしているだけである。従って適切に使わなければ問題が起きてしまう。見方を変えると、プリペアドステートメントはパラメーターとして渡す値にエスケープの必要がないから、たまたまSQLインジェクションの対策として利用できているだけに過ぎないという見方もできる。それを知った上で、ツールの限界を知ることが重要となる。プリペアドステートメントをメインの対策として教育するのであれば、適切に使うというのはどういうことか、あるいは反対にどのような場合に使えないか、使えない場合はどうするべきかということもしっかりと教えるべきだろう。結局穴がひとつでもあると問題なのだから、教育では対策を全部教えるべきであるのは間違いない。

大切なのは、漏れがないように対策を組み合わせるということだ。
  • 動的に組み立てる必要がないクエリではプリペアドステートメントを使う
  • プリペアドステートメントの元になる文字列を動的に組み立てつつ、識別子やキーワードはエスケープあるいはバリデーションする
  • 確実にエスケープまたはバリデーションしつつ、動的にクエリを組み立てる
SQLインジェクションを防ぐという点では、どの対策を選んでも論理的には間違いではない。Facebookのように究極のパフォーマンスを叩きださなければならないような現場もあるので、ウェブ界隈では案外動的なクエリ組み立ての需要もあるのではないかと思う。(サーバーサイドプリペアドステートメントはネットワークラウンドトリップが増えてしまうからだ。)

余談だが、LIKE句のエスケープはSQLインジェクションのエスケープとは目的と手法が異なる。ワイルドカードをエスケープしなかったら検索結果に影響が出るだけであって、SQLの文法への影響はない。これはこれで対策が必要である。

主張を比較するのは無意味

エスケープを先に教育すべきか、プリペアドステートメントの使用を徹底すべきかという論争は、どうも泥仕合の様相を呈しているように見える。個人的にはどちらの主張も間違ってないとは思うものの、実はあんまりしっくり来ない。どちらのほうが安全な教育方法かというのは論理的に証明できるものではないからだ。

論理的に証明できないのならば、ベンチマークをとって統計的にどちらが優秀なのかを示さなければならない。例えば新人1000人ずつに対して、それぞれの手法で教育を施し、その後それぞれの集団が何件のSQLインジェクションを生み出してしまったかというような実験をしなければ手法の優劣は比較できないはずだ。データ抜きで語ってしまうとお互いに水掛け論に終始してしまうことになる。データを見ずに議論することの不毛さを理解しているはずの技術者諸氏が水掛け論に終始し、ときには過激に相手を罵るかのような文章が見受けられるが、そのような場面を目にするのは悲しいものがある。

先ほど述べたように「どうすればSQLインジェクションを防げるか」という理論に正解はあっても、それを実践あるいは教育する方法に正解はないと言える。

余談になるが、一般的に議論において相手に対して攻撃的になるのは全くメリットがないし、ましてや人格を攻撃するのは論外である。相手をボロクソに言い負かせばその場ではスカッとするかも知れないが、実はあまり自らが得られる知見は少ないし、傍から見ていても気持ちの良いものではない。恐らく相手をボロクソに言い負かした以上に、自分自身にブーメランが突き刺さっているはずだ。

テストせよ

最近目にしたSQLインジェクション対策の記事では、どう書くとSQLインジェクションを事前に防ぐことができるかという議論に終始しているように思う。

だが待って欲しい。プログラムに欠陥があるかどうかを、そのコードから静的に判断するの果たして可能なのだろうか。もし、注意すれば欠陥がのないプログラムを書けるということであれば、バグなどというものは起こり得ないことになってしまう。だが、バグのないプログラムなど書けないことは皆さんよくご存知の通りである。いくら注意深くプログラムを書いても、バグをゼロにすることはできない。バグの種類がSQLインジェクションに対する脆弱性であっても、事前に完璧に防ぐことはできないというのは本来同じはずだ。

事前に問題を防ぐことができないのなら、やはりテストするしかない。SQLインジェクションであれば、自動的に検査するツールが多く出まわっているので、それらを用いてSQLインジェクションの危険性をできるだけ減らすべきなのだ。()昨今はユニットテストや回帰テストの重要性に対する認識が高まっているが、SQLインジェクションについても同様にテストが重要であるという認識が共有されればと思う。テストしないのはリスクを抱えるのと同じことだからだ。

プログラムの書き方をあれこれ議論するのも結構だが、どうテストするかというテーマのほうが建設的な議論になるように思う。テストも難しいテーマなので、そちらの議論のほうが世の中にとって有益なフィードバックになるだろう。

まとめ

SQLインジェクション対策は依然として難しいテーマであると思う。いくら気をつけてコーディングしたつもりでも、どこかに落とし穴が存在するということはあり得る話だ。その証拠に、未だSQLインジェクションの被害が後を絶たないではないか。

教育は確かに大事だ。だけどその効果を測定するのが難しい以上、その方法の正しさを議論するのはあまり意味がないように思う。どのようにすればSQLインジェクションを防げるかという要件さえ満たしていればそれで良いのではないだろうか。教え方は何通りもあるので、例え誰かの方法が自分の好みではなかったとしてもボロクソにこき下ろすような真似はするべきではないと思う。

SQLインジェクションも他の問題と同様、リスクを排除したければきちんとテストすべきだと思う。セキュリティ界隈の人たちからテスト関係の話(ツールの使用体験談とか、テストはパスしたけど攻撃を受けた例とか、オススメのツールはどれかとか)をもっと聞きたいと思う。

0 コメント:

コメントを投稿