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

The addslashes() Versus mysql_real_escape_string() Debate (Chris Shiflett: The PHP Blog) で文字コードによっては addslashes() による SQL のエスケープ処理は問題があることが指摘されていました。
日本語でも、Shift_JIS を扱っている場合は同様の問題が起きる可能性があるように思われましたので、メモしておきます。何か間違い、勘違いなどがありましたら指摘してください。おそらく、PHP だけの問題ではないと思います。

日本語でも文字コードが Shift_JIS の場合、addslashes() によるエスケープ処理では SQL インジェクションが可能になってしまうケースがあります。
例えば、上記のページの例を少し変更すると以下のようになります。

<?php
// 例: 文字コードが Shift_JIS の場合に問題となるケース

$_POST['username'] = "\x95' OR username = username /*";
$_POST['password'] = 'guess';

$mysql['username'] = addslashes($_POST['username']);
$mysql['password'] = addslashes($_POST['password']);

$sql = "SELECT *
	FROM   users
	WHERE  username = '{$mysql['username']}'
	AND    password = '{$mysql['password']}'";
?>

$sql の内容は以下のようになります(文字コードは Shift_JIS です)。

SELECT *
        FROM   users
        WHERE  username = '表' OR username = username /*'
        AND    password = 'guess'

実際の処理としては、以下のようになります。

  • '(シングルクォート) ã‚’ addslashes() 関数がエスケープを行い、\' に変換
  • 文字コードが Shift_JIS の場合、\' の前にある1バイト(\x95)と重なるとで意味を成す文字(\x95\x5c は Shift_JIS で 表 という文字)として認識される
  • その後ろにある '(シングルクォート)だけがエスケープされずに残る

以上から、' を有効にすることができ、その後ろに任意の SQL コードを挿入することが可能になります。
対策としては、Prepared Statement を使う、または、mysql_real_escape_string() などのデータベース専用のエスケープ関数を使用するという方法がありそうです(実際には試していませんので、確認した方が良いと思います)。

id:hoshikuzu さんがこの件について、まとめておられます(PHP 利用時に Shift_JIS で addslashes() によるエスケープ処理に SQL インジェクション可能な穴)。magic_quotes_gpc を有効にしている場合の問題について言及しておられます。magic_quotes_gpc が有効になっていると、クライアントから渡ってきた変数全てに、自動的に addslashes() を使用しているのと同じになります。
また、id:jrofbyr さんから以下のようなコメントをいただきました。MySQL は手元の環境にインストールしていませんので、確認できないのですが、mysql_real_escape_string() でも PEAR の Prepared Statement でもエスケープされない場合があるそうです。

mysql_real_escape_string()でもSET NAMES sjis;等を実行した後に使うと2バイト目の0x5C文字がエスケープされないようです。PEAR::DBのDB_common::prepare() ではMySQL使用時は内部でmysql_real_escape_string()が使われるので、エスケープされない場合がありました。確か環境は PHP 4.4.1/MySQL 4.1.12です。SET NAMES binary;でなんとか回避してます。

また、PostgreSQL でも SET client_encoding TO 'SJIS' を実行するとこの問題の影響を受けることを確認しました(PHP 5.1.2/PostgreSQL 8.0.4, データベースの文字コードは EUC-JP)。テストコードは以下の通りです。

<?php
$conn = pg_connect( "dbname=test user=test" );
if ( ! $conn ) {
    exit( 'Could not connect' );
}
pg_query( "CREATE TABLE id ( id int );
           INSERT INTO id (id) VALUES (0);
           INSERT INTO id (id) VALUES (1);
           CREATE TABLE t ( val text );
           INSERT INTO t (val) VALUES ('test');" );
pg_query( "SET client_encoding TO 'SJIS';" );

$input = "\x95'; SELECT * FROM id; --";
$query = "SELECT * FROM t WHERE val = '" . addslashes( $input ) . "';";
//$query = "SELECT * FROM t WHERE val = '" . pg_escape_string( $input ) . "';";

echo 'Query:' . $query . "\n";

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

結果は以下のようになりました(本来は結果は表示されないはず)。CLI版 の PHP 5.1.2 で実行しました。文字コードは Shift_JIS です。

SQL: SELECT * FROM t WHERE val = '表'; SELECT * FROM id; --';
Array
(
    [0] => Array
        (
            [id] => 0
        )

    [1] => Array
        (
            [id] => 1
        )

)

さらに、addslashes() の代わりに PostgreSQL の専用エスケープ関数の pg_escape_string() を使用しても SQL インジェクションができてしまいました。pg_escape_string() を使用すると、「'」は「''」に変換され、「SELECT * FROM t WHERE val = '\x95''; SELECT * FROM id; --';」になっているのですが、「\x95'」を SJIS の一文字として扱っているようで、「'」がエスケープされていないことになってしまうようです。

おそらく、日本語を扱う文字コードでこの問題の影響を受けるのは Shift_JIS だけだと思いますが、Shift_JIS 以外の他の文字コードでも同様の問題が起きる可能性があります。
安全な対処方法としては Shift_JIS などの文字の最後のバイトに \ が含まれる可能性のある文字コードを使用しないことです。
または、クライアントの文字コードに Shift_JIS を使用しないということでも問題を回避できるかもしれません。id:jrofbyr さんのコメントにあるように、MySQL では、SET NAMES binary; とすることで、とりあえずは回避できるそうです。他の方法しては、非効率ですが、データベースの文字コードが Shift_JIS でも、クライアントの文字コードを EUC-JP にして、SQL 文を EUC-JP に変換してから発行することは可能だと思います。