補足
この記事は旧徳丸浩の日記からの転載です。元URL、アーカイブ、はてなブックマーク1、はてなブックマーク2。備忘のため転載いたしますが、この記事は2008年6月2日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり
昨日のエントリ(徳丸浩の日記 - そろそろSQLエスケープに関して一言いっとくか - SQLのエスケープ再考)は思いがけず多くの方に読んでいただいた。ありがとうございます。その中で高木浩光氏からブクマコメントを頂戴した。
\がescape用文字のDBで\のescapeが必須になる理由が明確に書かれてない。\'が与えられたとき'だけescapeすると…。自作escapeは危うい。「安全な…作り方」3版で追加の「3.失敗例」ではDBで用意されたescape機能しか推奨していないこのうち、まず「\」のエスケープが必須となる(MySQLやPostgreSQLで)理由を説明しよう。
「\」をエスケープしないと処理がおかしくなる
MySQLにおいて、文字列「\n」は改行を意味する。その他、\に続く文字によって、様々な制御文字などを表現できるようになっている。このため、ユーザがたまたま入力欄などから「\n」と入力した際に、エスケープしないままだと、ユーザの意図に反して「\n」が改行に化けることになる。また、エスケープシーケンスとして定義されていない場合、たとえば「\x」は単に「x」を表すと規定されているので、「tokumaru.org大安売り¥100-」が「tokumaru.org大安売り100-」となり、「¥」が欠落してしまう。これはユーザの意図ではない。
このため、「\」を「\\」エスケープすることにより、上記のような文字化けを防ぐ必要がある。これが、そもそも「\」のエスケープが必須となる理由で、セキュリティ上の要求がなくても、必要な処理である。
「\」のエスケープもれによるSQLインジェクション
上記に加えて、「\」のエスケープが必要な状況でそれがもれている場合、SQLインジェクション脆弱性の原因となる。高木氏が指摘しておられるように、「\'」という入力に対して「'」のみエスケープすると、「\''」という文字列になる。前半の「\'」で「'」を表すので、末尾の「'」がエスケープされないで残ってしまう。つまり、文字列リテラルを終端できる。前回指摘したように、これによりSQL断片を埋め込むことが可能となる。この例だと分かりにくいので、もう少し現実的な攻撃パターンで説明しよう。上記でデータベースとしてはMySQLを想定している。「#」はMySQLでコメントを表すので、行末の「#'」は無視される。SELECT * FROM XXX WHERE NN='$id' $id として \'or 1=1# が入力されると \'or 1=1# ↓ エスケープ \''or 1=1# 元のSQLに適用すると、 SELECT * FROM XXX WHERE NN='\'' or 1=1#' すなわち、SQLの構文が改変された
Shift_JISでの問題
ここまでならまだよい。データベースの種類によっては「\」のエスケープを忘れないようにしようで済む。ところが、文字「\」を表す文字コード0x5cがShift_JISの2バイト目にも現れうることから話がややこしくなった(一方、「'」を表す0x27の方はShift_JISの二バイト目に現れない)。0x5cを二バイト目に含む文字は多数あるが、例として以下を紹介する。上記のように出現頻度の高い文字が含まれている。言語処理系やデータベースエンジン、APIなどに日本語処理の不完全な部分があると、SQLインジェクションの可能性が出てくる。ソ(835c) 能(945c) 表(955c) 予(975c)
データベースエンジンの日本語処理が不完全な場合
この場合は、「表'」のような組み合わせによりSQLインジェクションができる可能性がある。このように、末尾の「'」がエスケープされない状態となり、SQLインジェクション脆弱性が生まれる。↓フロント側でのエスケープ処理
表 ' 0x95 0x5c 0x27
↓データベース側の解釈
表 ' ' 0x95 0x5c 0x27 0x27
0x95 0x5c 0x27 0x27 0x95 \' で'一文字 ' がエスケープされずに余る
フロント側の日本語処理が不完全な場合
フロント側(言語処理系)の日本語処理が不完全な場合も「表'」の処理において「'」のエスケープ抜けが発生する上記はShift_JIS固有の現象であるので、できるだけShift_JIS以外の文字エンコード、例えばUTF-8を使うとよい。しかし、ケータイブラウザのようにShift_JISのみ受け付けるものや、エンタープライズ系の応用では文字化けを避ける目的でShift_JISを要求される場合もある(入出力時に文字コード変換して処理はUnicodeに統一する手もあるが、わずらわしい場合もあるだろう)。↓フロント側でのエスケープ処理(0x5cと0x27をそれぞれエスケープ)
表 ' 0x95 0x5c 0x27
↓データベース側の解釈
0x95 0x5c 0x5c 0x27 0x27
0x95 0x5c 0x5c 0x27 0x27 「表」一文字 \'で一文字 ' がエスケープされずに余る
PostgreSQLの対応
PostgreSQLでは上記の問題に対応するために、バージョン8.1.4(2006年5月24日リリース)では、以下のような変更が行われた。- 常にサーバ側で無効なコードのマルチバイト文字を拒否するように修正された
- 文字列リテラル中の安全でない「\'」を拒否する機能が追加された
- 以下略
pg_escape_stringの挙動調査
冒頭に紹介した高木浩光氏のブクマコメントの後半は、自作のエスケープではなくDBで用意されたエスケープ機能を利用するようにという指摘であった。まことにその通りで、私のブログを読むと、自作のエスケープを推奨しているようにも読めるが、それは良くない。それでは、PHPで用意されているpg_escape_stringは期待通り動作するのだろうか。簡単なスクリプトで検証してみた。
PHP言語側のエスケープの都合で紛らわしいが、入力文字列は「表\'」である。前回紹介したstandard_conforming_stringsの設定を正しく反映して、onの場合は「表\''」(「\」のエスケープをしない)、offの場合には「表\\''」(「\」、「'」ともエスケープする)結果となっている。いずれの場合にも「'」は「''」とエスケープされるので、backslash_quoteの設定には依存しない。素晴らしい。検証用スクリプト(PHP) $cn = pg_connect("host=localhost user=xxxx password=xxx"; echo pg_escape_string($cn, "表\\'"); 実行結果1 (standard_conforming_strings = on の場合) 表\'' 実行結果2 (standard_conforming_strings = off の場合) 表\\''
DBD::PgPPの場合
こんどはPerlでの例。PerlからPostgreSQLを利用する場合には、DBIとDBD::Pgの組み合わせが利用される・・・と思うのだが、筆者の環境では中々DBD::Pgがインストールできなかったので、代わりにDBD::PgPPを使って検証してみた。PgPPはピュアPerlで記述されたPostgreSQL用インターフェースである。DBD::PgPP中のquote()のソースを見ると、文字の変換部は以下のようになっていた(バージョン0.05)。正規表現の「?=」はゼロ文字の先読み表明というやつで、後ろに「\」か「'」が続くゼロ文字にマッチする。すなわち、「\」と「'」はそれぞれ「\\」と「\'」にエスケープされる。これはいただけない。standard_conforming_stringsもbackslash_quoteも無視されている。$s =~ s/(?=[\\\'])/\\/g; return "'$s'";
これは恐らくDBD::PgPPの完成度があまり高くないということなのだろう。従って、自作のエスケープをせずにDBで用意されたエスケープ機構を使えというガイドラインは一般論として正しいと思うが、上記のような例もあるので、初めて使う前に簡単なテストをしておけば安心できる。
まとめ
- 「\」のエスケープを要求するデータベースは日本語処理に特に注意
- 例えば、Shift_JISを避ける
- 自作のエスケープを避け、DBにて用意されたものを使う
- その場合でも過信は禁物で、できるならチェックしてから使うとよい