PostgreSQL で SJIS を使用した場合に SQL 文字列のエスケープが回避される可能性がある問題について

以前のメモにおいて、PHP から PostgreSQL への接続で文字コードSJIS(Shift JIS) を使用すると以下の問題があることを説明したのですが、分かりにくい点なども多いように思いますので、まとめ直しました。何か間違いや勘違いしているような部分がありましたら指摘してください。

問題としては、以下の2種類についてです。

  • addslashes() による SQL 文字列のエスケープ回避問題
  • PostgreSQL 専用関数でのエスケープ回避問題

addslashes() による SQL 文字列のエスケープ回避問題

少し複雑ですので、箇条書きにして記述してみます。この問題は PostgreSQL だけの問題ではありません。

  1. SJIS では、下位1バイトに 0x5c を含む文字がある(「ソ」「十」「表」など)
  2. 例えば、「表」という文字は SJIS で 0x95 0x5c になる(0x5c はバックスラッシュ(「\」)を表すエスケープ文字列)
  3. 文字コードSJIS を使用した場合、addslashes() で文字列をエスケープすると、0x95 0x5c 0x5c (「表\」)に変換される

例えば、実際に発生する問題として以下のような形で文字列のエスケープを行っているとします。

$test  = "\x95\x5c";	// SJIS で「表」という文字列
$query = "SELECT * FROM t1 WHERE a = '" . addslashes( $test ) . "'";
pg_set_client_encoding( 'SJIS' );
pg_query( $query );

addslashes( $test ) を評価した後の $query は以下のようになります。

"SELECT * FROM t1 WHERE a = '\x95\\'"

SJIS では、0x95 0x5c("\x95\") を一文字として認識します。
上記の場合、後ろのシングルクオートがエスケープされてしまい、SQL サーバ側では、構文エラーが発生します。

次に $test に "\x95'; SELECT * FROM t2 --" という文字列を与えると、SQL 文は以下のようになります。

"SELECT * FROM t1 WHERE a = '\x95\'; SELECT * FROM t2 --'"

この場合、0x95 0x5c が SJIS における一文字として認識され、その後ろにあるシングルクオートが有効になります。
これにより、後ろに任意の SQL 文を記述することが可能になります(SQL インジェクションが可能になります)。

PostgreSQL 専用関数でのエスケープ回避問題

この問題は PostgreSQL でしか確認していませんので、他の SQL サーバでも同様の問題があるかは分かりません。
PostgreSQL では、PostgreSQL 専用の文字列エスケープ関数(pg_escape_string() や pg_convert() など)を使用した場合でも、場合によっては SQL インジェクションが可能ということが後で分かりました。まとめると、以下のようになります。

問題としては、PHP から PostgreSQL に接続し、クライアントエンコーディングSJIS を使用した場合、正しいと考えられるエスケープ処理を行っていても SQL インジェクションが起きる可能性があるという点です。

問題が起きる可能性があるのは、以下の2つの条件が揃った場合です。

  • PHP から PostgreSQL に接続して、PHP で pg_set_client_encoding( 'SJIS' ) を実行する、あるいは "SET client_encoding TO 'SJIS'" という SQL 文を実行する
  • SQL 文字列のエスケープに、「'」 を 「''」 に変換する関数を使用する(pg_escape_string() や pg_convert()、PEAR::DB の escapeSimple() など)。

問題の詳細は以下の通りです。

  1. エスケープ時の文字列に 0x81 から 0x9F および 0xE0 から 0xFC(SJIS の上位 1バイト)が存在した場合、PostgreSQL はその後ろに続く文字列を合わせた 2バイトを合わせて一文字として認識する。
  2. 例えば 0x95 0x27 という文字列をエスケープすると、0x27(シングルクオート「'」) が シングルクオート2つに変換され、0x95 0x27 0x27 という 3バイトになる。
  3. PostgreSQL 側で 0x95 0x27 までが Shift JIS の一文字としてみなされ、残った 0x27 はシングルクオートとして有効になり、エスケープを回避されてしまう可能性がある(検証例としては、PostgreSQL 使用時の文字列のエスケープ回避問題を参照してください)。

この問題については、iakio日誌(2006-02-15)において、PHP スクリプトで回避する方法の例と、PostgreSQL に Patch を当てて回避する方法を示しておられますので、問題の影響を受ける場合は参考にすると良いと思います。

2006.03.09 追記

iakio さんから Patch を修正したとの連絡をいただきました。どうもありがとうございます。もし、以前に Patch を適用していた場合は再度適用し直しておいた方が良いと思います。また、この Patch は PostgreSQLCVS にコミットしていただいたそうですので、次のバージョンからはこの問題は解消されると思われます。
Patch を適用して、PostgreSQL 専用のエスケープ関数(pg_escape_string(), pg_convert()など)を使用しておけば、PostgreSQL 側でエラーになり、SQL が実行されなくなるため、クライアントエンコーディングSJIS にしていても SQL インジェクションを起こすことはできなくなると思います。ただし、addslashes() の問題は解消されませんので注意してください。