PreparedStatementを使用したSQLInjection対策

エスケープだけしてれば、セキュリティ対策が万全になる訳ではないですよ - masaのメモ置き場
の話について、高木さんより「PreparedStatementを使いなさい、動的パラメータの例は分岐で処理しなさい」、という趣旨のコメントを頂いてしまった。まだ、もやもや感があるので、もう少し考えてみる。

数値型チェックの話

select password from usertable where id = 入力値 (idが数値型)
入力値に 1 or 1 = 1 が与えられると・・

数値型の列に数値型以外の値が渡された時に発生する問題は、どの層ならば対策可能で、どの層が責任を持つべきなの?という話。型の概念を持つ言語なら、データベース層に数値型の引数を受け付けるインタフェースが用意されていて、アプリケーション層ではそのインタフェースを呼び出すことになる。型変換はアプリケーション層が行うことになるので、対策するのもアプリケーション層になると思ったのだけれど、なにかが違うのかもしれない。
あと、型の概念がない言語では危ないんじゃないかな?とも、思って書いたのだけれど、そもそも型の概念がない言語をあまり触ったことがないので、よくわからないことに気付いてしまった。この問題は、もうちょっと勉強してから考え直すことにする。

分岐で処理する

select * from usertable order by 動的パラメータ
動的パラメータに入力値をそのまま使用していると・・・

この例は、JavaのPreparedStatementを意識して書いてみた。動的パラメータとして列名と昇順、降順のに結びつくパラメータが渡される場合には、ホワイトリスト的なアプローチが必要。という趣旨のことを書いたのだが、分岐で処理するという言葉の方が確かに正しい。
ホワイトリスト的アプローチという記述だと

許可する列名:id,name
許可する並び替えの値:ASC,DESC
select * from usertable order by 許可された列名 許可された並び替えの値

と、いうイメージに結びつくが(入力値をそのまま使わないとしても)、これだとPreparedStatementが持つ準備済みSQLという意味にあまりそぐわない気がする。動的に生成しては、準備済みSQLではなくなってしまう。
それよりも

select * from usertable order by id ASC
select * from usertable order by id DESC
select * from usertable order by name ASC
select * from usertable order by name DESC

というSQLをあらかじめ準備しておいて、分岐で使用するSQLを切り分けた方がよさそうだ。

準備済みSQLで対応出来ない例

では、どんなケースでも準備済みSQLを用意しておくことが出来るのか?と言われたらそうとは言えないだろう。

select * from usertable where id in ( 動的パラメータ ID のリスト )

この例だと、動的パラメータ ID の数が不確定なため、準備済みSQLを用意することが出来ない。パラメータの数に対応したSQLをあらかじめ

パラメータが1つの時:select * from usertable where id in (?)を使ってパラメータバインド
パラメータが2つの時:select * from usertable where id in (?,?)を使ってパラメータバインド
パラメータが3つの時:select * from usertable where id in (?,?,?)を使ってパラメータバインド
以下永遠に続く

みたいな形で「準備」しておくという対応は乱暴すぎるだろう。従って、 動的パラメータ ID のリスト は自作の関数などによって展開する必要がある。自作の関数で処理するということになると、SQLInjection対策はPreparedStatementを使えばよい、の一言で片付けてよい問題では無くなってくる。

バインドメカニズムを利用したSQLInjection対策

自作の関数を実装するには厄介な問題がある。文字列型ならば、'を''にエスケープする関数を作成すればよいという訳でなく、\をエスケープシーケンスとして認識するDBMS実装依存の問題も考慮しなければならない。従って、エスケープ関数を自作という対応は出来るだけ避けるべきであろう。エスケープ関数を自作しない解決策としは、プレースホルダを動的に生成する関数を作成するという対応が考えられる。
引数として渡された動的パラメータIDの数が4個ならば、?,?,?,?という戻り値を返す関数を作成すれば、

select * from usertable where id in (?,?,?,?)

というSQLを動的に生成し、パラメータをバインドすることが可能となる。*1ユーザ入力値をそのまま使用して構文を作成していないため、安全性も確保されている。この例はSQLを動的に生成しているので、準備済みSQLを使用しているとは呼べないかもしれない。ただ、パラメータをバインドする仕組みは使用しているので、バインドメカニズムを利用したSQLInjection対策と呼ぶことが出来るだろう。

*1:Javaプレースホルダは?で統一されているのでこの対応が可能だが、ASP.NETは@nameの形式が取られているようなので、複雑な対策が必要かもしれない。