PostgrSQL 9.0から追加されたエスケープ関数から、SQLインジェクション対策を再度解説してみたいと思います。
SQLインジェクション対策の4原則
基本的にはSQLインジェクション対策として以下の原則を守っていれば、SQLインジェクションに脆弱なアプリケーションを作ることはありません。
- すべてのパラメータを文字列としてエスケープする
- すべてのパラメータをプリペアードクエリのパラメータとして処理する
- 文字エンコーディングの設定をAPIで行う
- パラメータとして処理できない文字列はバリデーションを行う
原則1と原則2は重複して適用する必要はありません。どちらかを行います。文字エンコーディングの設定やプリペアードクエリのエミュレーション・抽象化ライブラリのバグ等でSQLインジェクションが可能になる場合もありますが、通常であればこの原則を守っている限りSQLインジェクション脆弱性を作ることはありません。
アプリケーションからDBMSを利用している場合、文字エンコーディングはAPIを利用して変更しないとクライアント側(アプリ側)で適切なエスケープ方法が分からなくなります。このため、場合によっては文字エンコーディングを利用したSQLインジェクションが可能になります。MySQLやPostgreSQLの場合、SET NAMESやSET client_encoding TOは使ってはならない、ということです。セキュリティを意識していない入門書ではよくSQL文を利用した文字エンコーディング設定が利用されていますが、使ってはならない設定方法です。
パラメータとして処理できない文字列
「パラメータとして処理できない文字列」とはSQL文のクエリパラメータとならない文字列のことです。例えば、テーブル名、フィールド名などの識別子、LIMIT/OSFFSET/ASC/DESCなどのSQL語句はクエリのパラメータとなりません。このため、4原則の1、2によってエスケープしたり、プリペアードクエリのパラメータとして渡すことができませんでした。このため、4番目の「パラメータとして処理できない場合はバリデーションを行う」原則が必要でした。
典型的なSQLインジェクションに脆弱なコードは以下のようなコードです。
このコードは次のようなSQL文を生成することを想定しています。
しかし、識別子であるテーブル名とSQL語句のDESC/ASCになる部分は、文字列パラメータとしてエスケープすることもプリペアードクエリのパラメータとすることもできません。上記のようなコードではSQLインジェクションを自由に行えます。
例えば、$_GET['mytable']に
$_GET['order']に
を挿入すれば、mytableは削除できてしまいます。
プリペアードクエリの功罪
元々プリペアードクエリはセキュリティ対策としてではなく、SQLサーバの性能向上のために考案された仕組みです。SQL文とパラメータを分離することによりSQLサーバが同じSQL文を繰り返しパースすることなくSQLを実行できるようにします。繰り返しパースしない分、高速化できます[1]。SQL文とパラメータを分離したため、パラメータのエスケープを行わなくてもSQL文の構造を破壊するSQLインジェクションを防止できる、という作用もありました。クエリパラメータによるSQLインジェクションは防止できますが、プリペアードクエリはSQLインジェクションを完全に排除するセキュリティ対策として設計されていません。完全に排除できなくても仕方ないといえるでしょう。
筆者はSQLインジェクション対策の基本はエスケープであると言い続けていました。しかし、多くの開発者はプリペアードクエリさえ利用していれば大丈夫であるとしてエスケープ処理を軽んじてきました。例えば、PHPのMS SQL Serverのモジュールにはエスケープ関数がありません。SQLite3モジュールではSQLite2モジュールではあった文字列エスケープ関数がなくなり、プリペアードクエリ形式のクエリのみサポートされるようになりました[2]。
先ほどの例のようにテーブル名やSQL語句がパラメータとならないため、プリペアードクエリのみではSQLインジェクションが防げないことの理解は広がりつつあります。最近ではプリペアードクエリ万能論をあまり聞かなくなりました。しかし、データベースのクライアントライブラリには文字列のエスケープ関数さえないものもあるくらいです。「プリペアードクエリさえ使っていればSQLインジェクションはできない」という誤ったキャンペーンが大きすぎたので今でも誤解している開発者は多くいると思われます。
PostgreSQL 9.0 libpqの新機能
libpqとはPostgreSQLのクライアントアクセス用のライブラリです。エスケープ関数を削除してしまうDBMSもあるなか、PostgreSQL 9.0から新たなエスケープ関数が追加されました。
- PQescapeLiteral
-
PQescapeLiteralは、SQLコマンド内で使用するために文字列をエスケープします。これは、SQLコマンド内のリテラル定数としてデータ値を挿入する時に有用です。特定の文字(引用符やバックスラッシュ)は、SQLパーサによって特殊な解釈がなされないようにエスケープされなければなりません。PQescapeLiteralはこの操作を行います。
- PQescapeIdentifier
-
PQescapeIdentifierは、テーブル、列、関数名などのSQL識別子として使用できるように文字列をエスケープします。これはユーザが提供した識別子に、そのままではSQLパーサで識別子として解釈されない特殊な文字が含まれる可能性がある場合、または、大文字小文字の違いを維持しなければならない状況で識別子に大文字が含まれる可能性がある場合に有用です。
(PostgreSQL 9.0 日本語マニュアルより)
PQescapeLiteral
PQescapeLiteralは名前の通りパラメータを文字リテラル(文字列のパラメータ)としてエスケープをする関数です。この関数を使うと従来
としていた操作を、たとえば
とするだけで済むようになります。随分スッキリしますが、PHPのpgsqlモジュールにこの関数はまだありません。
この関数は見た目をスッキリさせるだけでなく「すべて」のリテラルを文字リテラルとしてエスケープすべきSQLインジェクション対策の4原則の1番目を開発者に理解してもらうためにも有用な関数です。エスケープすべきは文字列リテラル(文字列パラメータ)だけではありません。数値、日付もエスケープすべきリテラルです。
PQescapeIdentifier
PQescapeIdentifierはテーブル名やフィールド名などの識別子をエスケープする関数です。すべてのパラメータをプリペアードクエリのパラメータとして渡している場合であっても、この関数は必要な関数でしたが今まで存在しませんでした。PQescapeIdentifier関数もPQescpaeLiteralと同様に利用できます[3]。たとえば、
となります。PHPのpgsqlモジュールにこの関数はまだありませんが、アイデアは理解できたと思います。
先ほどのSQLインジェクションの例に出てきたORDER BY句のDESC/ASCは、このPQescapeIdentifier関数でもエスケープできません。DESC/ASCは識別子(Identifier)ではなくSQL語句だからです。つまり、PQescapeIdentifier関数のラッパー関数が利用できるようになっても、SQLインジェクション対策の4原則の4番目は変わらず必要であるということです。SQL語句の語彙は限定されているのでバリデーションするのは簡単です。問題となることは少ないと思います。
まとめ
SQLインジェクション対策の基本は「エスケープ」です。「プリペアードクエリ」ではありません。正しく「エスケープ」と「プリペアードクエリ」、「バリデーション」を利用してSQLインジェクションを完全に撲滅しましょう。
数値などを文字リテラルとして扱うとSQL文のパースに若干余分な時間が必要になります。文字リテラルはパーサによって別途パースされることがオーバーヘッドの理由です。しかし、そもそもパフォーマンスが気になるアプリケーションでは元々パフォーマンス向上のために考案されたプリペアードクエリをクエリを利用します。プリペアードクエリを利用すると、アプリケーションによっては倍以上のスループットとなる場合も少なくありません。
PostgreSQLプロジェクトでは正しいSQLインジェクション対策が行えるよう識別子をエスケープ処理できる専用のエスケープ関数が追加されました。まだMS SQL ServerやSQLite3のクライアントライブラリではSQLインジェクション対策の基本であるエスケープ関数がない状況です。しかし、今後正しいSQLインジェクション対策が行えるようほかのDBMSもPostgreSQL 9.0にならって普及する可能性があります。注目したいPostgreSQL 9.0の追加仕様の紹介でした。