確認環境
MySQL 5.5.27
PHP 5.4.7
現象
PDO::setAttribute( ATTR_EMULATE_PREPARES, false )
をせず、prepareしても静的プレースホルダは使用されない。
確認
<?php
$dsn = 'mysql:dbname=test;host=localhost';
$user = 'root';
$password = '';
try{
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sql = 'select blog_id, blog_name from blogs where blog_name like ?';
$stmt = $pdo->prepare($sql);
$stmt->execute(array('%my%'));
}catch (PDOException $e){
print('Error:'.$e->getMessage());
die();
}
結果
MySQLのクエリーログで確認
setAttribute(PDO::ATTR_EMULATE_PREPARES, true)またはデフォルトの場合:
130722 14:40:59 2 Connect root@localhost on test
2 Query select blog_id, blog_name from blogs where blog_name like '%my%'
2 Quit
setAttribute(PDO::ATTR_EMULATE_PREPARES, false)した場合:
130722 14:41:26 3 Connect root@localhost on test
3 Prepare select blog_id, blog_name from blogs where blog_name like ?
3 Execute select blog_id, blog_name from blogs where blog_name like '%my%'
3 Close stmt
3 Quit
まとめ
SQLインジェクションの対策としてプリペアドステートメントの使用が推奨されますが、PDOにおいてデフォルトではPDO::ATTR_EMULATE_PREPARESがtrueに設定されており、この状態ではprepareでプレースホルダを指定してもエミュレートされ、実際にMySQLにわたる際にはプリペアドステートメントではなくなってしまいます(普通にSQL発行するのと同じ)。この状態では、 自動エスケープはされるものの 、SQLインジェクションの危険があると思います。
(2013.07.22 19:14 訂正: 「追記」を参照してください。)
PDOで静的プレースホルダを使用する場合(少なくともMySQLでは)、PDO::ATTR_EMULATE_PREPARESを「false」にするべきでしょう。
(え、常識?)
(2013.07.22 18:57 追記)
ATTR_EMULATE_PREPARESがデフォルトの状態(true)でエミュレーションが効いている場合、ドライバ側で自動エスケープしてくれるみたいです。
だから安心かというとそうではなくて、文字コード変換を利用した下記攻撃でSQLインジェクションできてしまいました。
ちなみにATTR_EMULATE_PREPARESをfalseにすると下記プログラムでもTRUNCATEは実行されません(SQLインジェクション対策できている)。
$dsn = 'mysql:dbname=test;host=localhost';
$user = 'root';
$password = '';
try{
$pdo = new PDO($dsn, $user, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES sjis');
$attack = "\x97' OR 1=1; truncate blogs; #";
$sql = 'select blog_id, blog_name from blogs where blog_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute(array($attack));
while($result = $stmt->fetch(PDO::FETCH_ASSOC)){
print_r($result);
}
}catch (PDOException $e){
print('Error:'.$e->getMessage());
die();
}