PostgreSQL 使用時の文字列のエスケープ回避問題

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


1月22日に書いた、addslashes() による SQL 文字列のエスケープ回避問題の続きです。PostgreSQL でさらに検証してみました。

結論としては、前回と同様ですが、PostgreSQL に関しては、SJIS は使用しない方が安全という事になります。

クライアントの文字コードとして、SJIS を使用している場合、addslashes() によるエスケープは既に指摘されている通りですが、PostgreSQL 用の文字列エスケープ関数である、pg_escape_string() を使用している場合でも問題があります。
また、その他の PostgreSQL の一部の関数でも影響を受けることを確認しました。ただし、PHP 5.1.0 以降で導入された pg_prepare() と pg_execute() を使用した場合は、問題ないようです。
使用した PHPPostgreSQL のバージョンは PHP 5.1.2 と PostgreSQL 8.1.3 ですが、他のバージョンでも同様の問題があると考えられます。

検証

以下は検証です。
まず、test というデータベースに以下のテーブルを作成します(文字コードEUC_JP になっています)。

test=> SELECT * FROM t1;
 val
-----
 OK
(1 row)

test=> SELECT * FROM t2;
 val
-----
 NG
(1 row)

PHP 5.1.2 で以下のようなスクリプトを作成して検証しました。

<?php
$conn = pg_connect( "host=localhost dbname=test user=test" );
if ( ! $conn ) { exit( 'Could not connect' ); }

// クライアントの文字コードを SJIS に変更
pg_set_client_encoding( 'SJIS' );
echo "Client Encoding:" . pg_client_encoding() . "\n";

// SQL インジェクション検証用文字列
$input = "\x95'; SELECT * FROM t2; --";

// 文字列のエスケープに pg_escape_string() を使用
$query = "SELECT * FROM t1 WHERE val = '" . pg_escape_string( $input ) . "';";

echo "SQL: " . $query . "\n";

$result = pg_query( $query );
if ( ! $result ) { exit( "Query failed\n" ); }

// 結果表示
echo "Result: \n";
var_dump( pg_fetch_all( $result ) );
?>

結果は以下の通りになりました。本来は bool(false) が結果として表示されるはずです。

Client Encoding:SJIS
SQL: SELECT * FROM t1 WHERE val = ''; SELECT * FROM t2; --';
Result:
array(1) {
  [0]=>
  array(1) {
    ["val"]=>
    string(2) "NG"
  }
}

原因は、0x9527 という文字列「\x95'」 は SJIS の範囲内ではないのですが、これが SJIS として扱われているという点にあるようです。

つまり、\x95' を pg_escape_string() でエスケープ処理を行うと、「'」は「''」に変換され、\x95'' になり、上のコードでは、エスケープ処理後、以下のような SQL が構築されています。

SELECT * FROM t WHERE val = '\x95''; SELECT * FROM id; --';

PostgreSQL 側が「\x95'」を SJIS の一文字として扱ってしまうため、「'」を回避して、任意の SQL をその後ろに記述することが可能になっています。

内部で pg_convert() を使用する関数の場合(例: pg_select() )

問題は、pg_escape_string() だけではなく、pg_convert() および、pg_convert() を内部で使用している関数も影響を受けることを確認しました。

例えば、以下のように、pg_select() を使用したコードを実行します。

<?php
$conn = pg_connect( "host=localhost dbname=test user=test" );
if ( ! $conn ) { exit( 'Could not connect' ); }

pg_set_client_encoding( 'SJIS' );
$input = "\x95'; SELECT * FROM t2; --";

print_r( pg_select( $conn, 't1', array( "val" => $input ) ) );
?>

結果は以下の通りです。

Array
(
    [0] => Array
        (
            [val] => NG
        )

)

PEAR::DB の Prepared Statement を使用した場合

また、PEAR::DB の Prepared Statement を使用した場合でも問題がありました。

<?php
require( 'DB.php' );

$dsn = 'pgsql://postgres@localhost/test';
$db =& DB::connect( $dsn );

$db->query( "SET client_encoding TO 'SJIS'" );

$dh  = $db->prepare( "SELECT * FROM t1 WHERE val = ?" );
$res = $db->execute( $dh, "\x95'; SELECT * FROM t2; --" );

if ( PEAR::isError( $res ) ) {
    exit( $res->getMessage() . "\n" );
}
while ( $res->fetchInto( $row ) ) {
    echo $row[0] . "\n";
}
?>

PEAR::DB ではクライアントの文字コードを指定する関数がありませんでしたので、「SET client_encoding TO 'SJIS'」 というクエリを発行しています。
結果

NG

pg_prepare() と pg_execute() を使用した場合

PHP 5.1.0 から導入された PostgreSQL の Prepared Statement を使用する関数である、pg_prepare() と pg_execute() を使用した場合は、このような問題がないことを確認しました。

<?php
$conn = pg_connect( "host=localhost dbname=test user=test" );
if ( ! $conn ) {
        exit( 'Could not connect' );
}
pg_set_client_encoding( "SJIS" );

$result = pg_prepare( $conn, 'query', 'SELECT * FROM t1 WHERE val = $1' );
$result = pg_execute( $conn, 'query', array( "\x95'; SELECT * FROM t2; --" ) );
var_dump( pg_fetch_all( $result ) );
?>

結果

bool(false)

まとめ

以上より、現状では、PHP 5.1.0 より前のバージョンで PostgreSQL でクライアントの文字コードSJIS を使用しない方が良いと考えられます。
PHP 5.1.0 以上では pg_prepare() や pg_execute() を使用して SQL を発行すればこの問題は回避できるようですが、他の関数では影響を受けますので、気を付ける必要があります。