数値項目に対するSQLインジェクション対策

文字列項目に対するSQLインジェクション対策は、「'」(シングルクォート)や「\」(円マーク、バックスラッシュ)のエスケープであるが、数値リテラルなどはエスケープでは対策できない。
ここで、なぜ文字列に対してエスケープ処理が対策になるかを復習しておこう。それは「どんな文字(列)に対しても正しいSQL文を生成する」ためである。一方、数値の場合は、どんな数値であってもエスケープ処理などは元々必要ない。それにも関わらずSQLインジェクション脆弱性が混入するのは、数値を想定した変数に数値以外の文字が混入するからに他ならない。
すなわち、文字列の場合と数値の場合は、対策の前提が異なるわけである。
ここで、高木浩光氏からの批判に戻ると、関連する内容は以下のとおりである。

「対策は入力値の妥当性検証」 < それは違う。SQL文構成時直前に型変換(ないし型検査)する。文字列のクオート同様

以下、いくつかの論点から検討してみよう。

そもそも妥当性検証は必須

SQLインジェクション対策以前の問題として、アプリケーションは正しく動かなければならない。インジェクション系脆弱性に対しては、正しく動くプログラムを書いていれば、結果として脆弱性対策にもなっている。文字列項目に対するエスケープ処理は、その典型的な例である。
数値項目に対してはどうだろうか。数値を前提とした変数に数値以外の文字が混入した状態はエラーとすべきであって、処理を継続してはならない。たとえば、購入商品の数量が「1 or TRUE」などとなっていたら、処理を継続するのはおかしい。
ここで、サニタイズ的な発想だと「数値以外の文字を除去する」というアプローチになるのだろうが、これも賛同できない。たとえば、「1 or 1=1」というデータから数値以外の項目を削除すると「111」になる。数量111で処理を継続するというのも、アプリケーション要件としてはおかしいだろう。
いや、それは言いがかりであって、「1 or 1=1」をサニタイズすると「1」になるのが正しいのだという反論もあるかもしれない。しかし、ユーザの入力その他を断りもなく改変しておいて、「正しい」も「正しくない」もなかろうと私は思う。
というわけで、SQLインジェクション対策以前の問題として、そもそも入力値の妥当性検証(数値としての)は必要である。これは、結果としてSQLインジェクション対策にもなっているわけだが、問題はこれをどう考えるかである。

「変数に型のない言語」におけるデータ型の意味

ここで「変数に型のない言語」について、若干の分類を試みよう。といっても無学もののことゆえ、そんなに何種類もの言語を知っているわけではないので、PerlPHPJavaScriptRubyなどを例として取り上げる。
上に挙げた言語の中で、Rubyは変数にこそ型がないが、データ(オブジェクト)は型をしっかりと持っている。そのため、型変換や型チェックということが意味を持つ。
一方、PerlPHPは、内部的には型を持っているのだろうが、現実のプログラミング上は実に「柔軟に」運用されているので、現実問題としてはデータにも型がないように見える。しばしば、数値15と文字列"15"は同じように扱われる。Perlの場合、型によって演算子を分けている(とくに比較演算子)ことからも上記は裏付けられる。
JavaScriptに至ってはいい加減もいいところで、演算子を分けるのではなく、文脈によって適当に処理が分かれる。

"8" + "5" --> 85となる(文字列連結)
"8" * "5" --> 40となる(乗算)

つまり、Ruby以外の、PerlPHPJavaScriptなどは、内部で持っているであろう型の情報ではなく、変数の内容と演算の種類(文脈)によってデータの型が判断される。すなわち、これら言語(Ruby以外)は型のチェックや型変換は本質的に意味を持たない。

出力時に検証する必要性

Rubyなどは別として、Perl/PHP/JavaScriptなど一般によく普及した「変数に型のない言語」では、「型チェック」はあまり意味を持たず、その内容のチェックに意味がある。変数に数値として正当な内容になっているかをチェックすれば、それはすなわち「妥当性検証(Validation)」である。
ここで、再びSQLインジェクション対策に話題を戻す。ITproの連載にも書いたことだが、インジェクション系脆弱性対策の基本は、「出力時のエスケープ」である。出力時に行う意味は連載に書いたので繰り返さないが、ここで注目しておかなければならない理由として、「脆弱性対策が実施されていることの検証」が行いやすくなる点がある。
すなわち、ソース上でSQLを発行している箇所があったとして、その直前部分をチェックしてエスケープ処理がされていれば、SQLインジェクション対策がなされていることを検証しやすい。しかるに、数値項目に関しては、妥当性検証がなされているかどうかを確認するためには、ソースコード上を延々と遡っていかなければならない。これは骨のおれる作業であるし、見逃しや過剰検知の可能性もある。
つまり、値の妥当性検証を行うにしても、(SQL発行など)出力時に行っていれば、脆弱性対策の有無が確認しやすいし、ひいてはアプリケーションの保守性も高まることになる。

では、どう書けばよいのか

ここまでの話題をいったんまとめよう。

  • 変数に型のない言語では、数値データに対する妥当性検証が重要である
  • インジェクション対策としては、出力時に検証することが望ましい

しかし、アプリケーション開発の原則に立ち返ると、データの妥当性検証は入力時(正確にはデータの発生後すぐ)に行うべきものである。妥当性検証はSQLインジェクション対策のためだけに行うのではなく、処理の正当性を保証するために重要な処理であるからである。
しかも、同じ変数に対して、何度もSQLが発行されるケースも珍しくない。
今までの議論から、結果として、データの妥当性検証は、入力時に一回、SQL発行時にそれぞれ実施することが望ましいことになる。

最初に一回だけやればいいじゃないか

と私も思わないでもないのだが、結論としては、「必要の都度何度でも妥当性検証する」のが正解であろう。
ここで、何度も妥当性検証することのデメリットを挙げておくと、

  • 実行速度が遅くなる(理由A)
  • プログラムがバッチクなる(理由B)

理由Aについては、無視してもよいだろう。妥当性検証に要するコストは無視できる。
このように書くと「前に書いたことと違う」と思われるかもしれないが、そうではない。データベース・サーバーとは違ってWebサーバー(アプリケーションサーバー)はスケールアウトによる負荷分散が容易であるし、「暗黙の型変換」によりインデックスが無効になるなどの副作用もないからである。現実問題としてもValidationに要する処理時間の影響は軽微であろう。
理由Bについては、私もチクッと胸が痛まないでもないのだが、次のように考えたらどうだろう。

  • プログラムがバッチくならないようプログラミングを工夫する
  • 文字列の場合はエスケープするわけだから、数値の妥当性検証とあわせて、美しいシンメトリーを構成するのだと思い込む

プログラミングの工夫はどのような場合でも重要であるが、このケースは、検査や保守の容易性が目的なのでなおさらである。例外処理などをうまく使えば、それほどプログラムが汚くならずに記述できる方法はある。
シンメトリーに関して言えば、データが文字列の場合はエスケープ処理の関数を呼び、数値の場合は何もしないというのではかえって美しくないと考えるのである。これは現実的な意味があって、ある変数にエスケープ処理がなされていない場合、単にエスケープ漏れなのか、数値項目ゆえにエスケープしていないのかをソースコード上で判別することは煩雑である。すべての変数や式について、エスケープあるいは数値チェックの関数を呼ぶようにしておけば、このような煩雑さは軽減できる。
残った問題は、呼ぶべき関数を間違えている場合だが・・・既に長くなっているので、これは「宿題」ということにしておこう。

結論としては、高木浩光氏の書いていることと私の意図はそれほど乖離していないと思う。連載では紙面の制約もあって、簡潔な記述にならざるを得なかったことをお詫びする。ここはブログなので、意図を思う存分書かせていただいた。

余談

胸がチクっとする本当の理由は、「必要の都度何度でも妥当性検証」したら、変数に型のない言語の持つ軽快さが失われ、Javaなど変数に型のある言語を使うべきではないか、と思うからだが、この話題は私の手には負えそうもない。