简短的答案是YES,如果使用得当,PDO 准备是足够安全的。
我正在适应这个答案 https://stackoverflow.com/a/12118602/338665谈谈PDO...
长答案并不那么容易。它基于攻击在这里展示 http://shiflett.org/blog/2006/jan/addslashes-versus-mysql-real-escape-string.
攻击
那么,让我们首先展示攻击......
$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));
在某些情况下,这将返回多于 1 行。让我们剖析一下这里发生了什么:
-
选择字符集
$pdo->query('SET NAMES gbk');
为了使这种攻击起作用,我们需要服务器期望在连接上进行编码'
如 ASCII 所示,即0x27
and某些字符的最后一个字节是 ASCII\
i.e. 0x5c
。事实证明,MySQL 5.6 默认支持 5 种这样的编码:big5
, cp932
, gb2312
, gbk
and sjis
。我们会选择gbk
here.
现在,注意使用非常重要SET NAMES
这里。这设置了字符集在服务器上。还有另一种方法可以做到这一点,但我们很快就会实现。
-
有效负载
我们将用于此注入的有效负载以字节序列开始0xbf27
. In gbk
,这是一个无效的多字节字符;在latin1
,这是字符串¿'
。请注意,在latin1
and gbk
, 0x27
其本身就是一个字面意思'
特点。
我们选择这个有效负载是因为,如果我们调用addslashes()
在它上面,我们插入一个 ASCII\
i.e. 0x5c
, 之前'
特点。所以我们最终会得到0xbf5c27
,其中在gbk
是一个两个字符的序列:0xbf5c
其次是0x27
。或者换句话说,一个valid字符后跟未转义的字符'
。但我们没有使用addslashes()
。那么继续下一步...
-
$stmt->执行()
这里要认识到的重要一点是,PDO 默认情况下会执行以下操作:NOT做真正准备好的陈述。它模拟它们(对于 MySQL)。因此,PDO 在内部构建查询字符串,调用mysql_real_escape_string()
(MySQL C API 函数)每个绑定字符串值。
C API 调用mysql_real_escape_string()
不同于addslashes()
因为它知道连接字符集。因此它可以对服务器期望的字符集正确执行转义。然而,到目前为止,客户认为我们仍在使用latin1
对于这种联系,因为我们从未另外说过。我们确实告诉了server我们正在使用gbk
,但是client仍然认为是latin1
.
因此调用mysql_real_escape_string()
插入反斜杠,我们就有了一个自由悬挂'
我们“逃脱”内容中的角色!事实上,如果我们看一下$var
in the gbk
字符集,我们会看到:
縗' OR 1=1 /*
这正是攻击所需要的。
-
查询
这部分只是一种形式,但这是呈现的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚使用 PDO 准备语句成功攻击了一个程序...
简单的修复
现在,值得注意的是,您可以通过禁用模拟准备语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这会usually产生真正的准备好的语句(即数据在与查询不同的数据包中发送)。但是,请注意,PDO 会默默地fallback https://github.com/php/php-src/blob/master/ext/pdo_mysql/mysql_driver.c#L210模拟 MySQL 本身无法准备的语句:它可以的语句是listed http://dev.mysql.com/doc/en/sql-syntax-prepared-statements.html手册中,但要注意选择适当的服务器版本)。
正确的修复方法
这里的问题是我们使用了SET NAMES
而不是 C APImysql_set_charset()
。否则的话,攻击就不会成功。但最糟糕的是 PDO 没有公开 C APImysql_set_charset()
直到 5.3.6,所以在之前的版本中cannot针对每个可能的命令防止这种攻击!
现在它被曝光为DSN参数 http://www.php.net/manual/en/ref.pdo-mysql.connection.php,应该使用代替 SET NAMES
...
这是假设我们使用的是 2006 年以来的 MySQL 版本。如果您使用的是早期的 MySQL 版本,那么bug http://bugs.mysql.com/bug.php?id=8378 in mysql_real_escape_string()
意味着无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节以用于转义目的即使客户端已被正确告知连接编码所以这次攻击还是会成功的。 MySQL 中的错误已修复4.1.20 http://dev.mysql.com/doc/refman/4.1/en/news-4-1-20.html, 5.0.22 http://dev.mysql.com/doc/relnotes/mysql/5.0/en/news-5-0-22.html and 5.1.11 http://dev.mysql.com/doc/relnotes/mysql/5.1/en/news-5-1-11.html.
拯救的恩典
正如我们一开始所说的,要使这种攻击起作用,数据库连接必须使用易受攻击的字符集进行编码。utf8mb4 http://dev.mysql.com/doc/en/charset-unicode-utf8mb4.html is 不脆弱但还可以支持everyUnicode 字符:因此您可以选择使用它,但它仅从 MySQL 5.5.3 开始可用。另一种选择是utf8 http://dev.mysql.com/doc/en/charset-unicode-utf8.html,这也是不脆弱并且可以支持整个Unicode基础多语种飞机 http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane.
或者,您可以启用NO_BACKSLASH_ESCAPES http://dev.mysql.com/doc/en/sql-mode.html#sqlmode_no_backslash_escapesSQL 模式,它(除其他外)改变了mysql_real_escape_string()
。启用此模式后,0x27
将被替换为0x2727
而不是0x5c27
以及转义过程cannot在以前不存在的任何易受攻击的编码中创建有效字符(即0xbf27
还是0xbf27
等)——因此服务器仍然会拒绝该字符串,因为该字符串无效。不过,请参阅@eggyal 的回答 https://stackoverflow.com/a/23277864/623041针对使用此 SQL 模式(尽管不是使用 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 始终执行真正的准备好的语句。
包起来
If you:
- 使用现代版本的 MySQL(5.1 晚期、所有 5.5、5.6 等)ANDPDO 的 DSN 字符集参数(在 PHP ≥ 5.3.6 中)
OR
- 不要使用易受攻击的字符集进行连接编码(您只使用
utf8
/ latin1
/ ascii
/ etc)
OR
- Enable
NO_BACKSLASH_ESCAPES
SQL模式
你100%安全。
否则你很脆弱即使您正在使用 PDO 准备好的语句...
Addendum
我一直在慢慢地开发一个补丁,以将默认值更改为不模拟,为未来版本的 PHP 做好准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。因此这可能会导致问题(这也是测试失败的部分原因)。