简短的回答是肯定的,如果使用得当,PDO准备是足够安全的。
我用这个答案来讨论PDO…
要回答这个问题并不容易。它是基于这里演示的攻击。
这次袭击
所以,让我们从展示攻击开始…
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
在某些情况下,它会返回多于一行。让我们来分析一下这里发生了什么:
Selecting a Character Set
$pdo->query('SET NAMES gbk');
For this attack to work, we need the encoding that the server's expecting on the connection both to encode ' as in ASCII i.e. 0x27 and to have some character whose final byte is an ASCII \ i.e. 0x5c. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5, cp932, gb2312, gbk and sjis. We'll select gbk here.
Now, it's very important to note the use of SET NAMES here. This sets the character set ON THE SERVER. There is another way of doing it, but we'll get there soon enough.
The Payload
The payload we're going to use for this injection starts with the byte sequence 0xbf27. In gbk, that's an invalid multibyte character; in latin1, it's the string ¿'. Note that in latin1 and gbk, 0x27 on its own is a literal ' character.
We have chosen this payload because, if we called addslashes() on it, we'd insert an ASCII \ i.e. 0x5c, before the ' character. So we'd wind up with 0xbf5c27, which in gbk is a two character sequence: 0xbf5c followed by 0x27. Or in other words, a valid character followed by an unescaped '. But we're not using addslashes(). So on to the next step...
$stmt->execute()
The important thing to realize here is that PDO by default does NOT do true prepared statements. It emulates them (for MySQL). Therefore, PDO internally builds the query string, calling mysql_real_escape_string() (the MySQL C API function) on each bound string value.
The C API call to mysql_real_escape_string() differs from addslashes() in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1 for the connection, because we never told it otherwise. We did tell the server we're using gbk, but the client still thinks it's latin1.
Therefore the call to mysql_real_escape_string() inserts the backslash, and we have a free hanging ' character in our "escaped" content! In fact, if we were to look at $var in the gbk character set, we'd see:
縗' OR 1=1 /*
Which is exactly what the attack requires.
The Query
This part is just a formality, but here's the rendered query:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜你,你刚刚成功地攻击了一个使用PDO准备语句的程序…
简单的解决方法
现在,值得注意的是,你可以通过禁用模拟预处理语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这通常会导致一个真正的预处理语句(即数据从查询中以单独的数据包发送)。但是,请注意PDO将会无声地退回到模拟MySQL不能在本地准备的语句:手册中列出了那些它可以准备的语句,但要注意选择适当的服务器版本)。
正确的解决方法
这里的问题是我们使用了SET NAMES而不是C API的mysql_set_charset()。否则,攻击不会成功。但最糟糕的是,PDO直到5.3.6才公开mysql_set_charset()的C API,所以在以前的版本中,它不能防止每个可能的命令都受到这种攻击!
它现在被暴露为一个DSN参数,应该使用它而不是SET NAMES…
前提是我们使用的是2006年以后的MySQL版本。如果您使用的是较早的MySQL版本,那么mysql_real_escape_string()中的一个错误意味着无效的多字节字符(例如我们的有效负载中的那些字符)被当作单字节进行转义,即使客户端已经正确地通知了连接编码,因此这种攻击仍然会成功。该错误已在MySQL 4.1.20, 5.0.22和5.1.11中修复。
可取之处
正如我们在开始时所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4不容易受到攻击,但可以支持所有Unicode字符:所以你可以选择使用它,但它只在MySQL 5.5.3之后可用。另一种替代方法是utf8,它也不容易受到攻击,并且可以支持整个Unicode基本多语言平面。
Alternatively, you can enable the NO_BACKSLASH_ESCAPES SQL mode, which (amongst other things) alters the operation of mysql_real_escape_string(). With this mode enabled, 0x27 will be replaced with 0x2727 rather than 0x5c27 and thus the escaping process cannot create valid characters in any of the vulnerable encodings where they did not exist previously (i.e. 0xbf27 is still 0xbf27 etc.)—so the server will still reject the string as invalid. However, see @eggyal's answer for a different vulnerability that can arise from using this SQL mode (albeit not with PDO).
安全的例子
下面的例子是安全的:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
因为服务器期望utf8…
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
因为我们已经正确地设置了字符集,所以客户端和服务器匹配。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
因为我们已经关闭了模拟的准备语句。
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
因为我们已经正确地设置了字符集。
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
因为MySQLi一直在执行真准备语句。
结束
如果你:
使用现代版本的MySQL(5.1后期,所有5.5,5.6等)和PDO的DSN字符集参数(在PHP≥5.3.6)
OR
不要使用易受攻击的字符集进行连接编码(你只使用utf8 / latin1 / ascii / etc)
OR
开启NO_BACKSLASH_ESCAPES SQL模式
你100%安全。
否则,即使您使用PDO预处理语句,您也很容易受到攻击…
齿顶高
我一直在慢慢地编写一个补丁,将默认值更改为不模拟,为PHP的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都中断了。一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这可能会导致问题(也是测试令人厌烦的部分原因)。