假设我有这样的代码:
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDO 文档说:
准备好的语句的参数不需要引用;司机为您处理。
这真的是我需要做的一切来避免 SQL 注入吗?真的那么容易吗?
如果它有所作为,您可以假设 MySQL。另外,我真的只是对使用准备好的语句来对抗 SQL 注入感到好奇。在这种情况下,我不关心 XSS 或其他可能的漏洞。
原文由 Mark Biek 发布,翻译遵循 CC BY-SA 4.0 许可协议
简短的回答是 肯定 的,如果使用得当,PDO 准备就足够安全了。
我正在调整 这个答案 来谈论 PDO ……
长答案并不容易。它基于 此处演示 的攻击。
攻击
所以,让我们从展示攻击开始……
在某些情况下,这将返回超过 1 行。让我们剖析一下这里发生了什么:
为了使这种攻击起作用,我们需要服务器期望在连接上的编码既编码
'
如 ASCII 即0x27
并且 具有一些最终字节为 ASCII 的字符\
即0x5c
。 As it turns out, there are 5 such encodings supported in MySQL 5.6 by default:big5
,cp932
,gb2312
,gbk
andsjis
。我们将在这里选择gbk
。现在,注意这里使用
SET NAMES
非常重要。这会 在服务器 上设置字符集。还有另一种方法,但我们很快就会到达那里。我们将用于此注入的有效负载以字节序列
0xbf27
。在gbk
中,这是一个无效的多字节字符;在latin1
中,它是字符串¿'
。请注意,在latin1
和gbk
,0x27
中,它本身就是一个文字'
字符。这里要意识到的重要一点是,PDO 默认情况下 不会 执行真正的准备好的语句。它模拟它们(对于 MySQL)。因此,PDO 在内部构建查询字符串,对每个绑定的字符串值调用
mysql_real_escape_string()
(MySQL C API 函数)。对 --- 的 C API 调用与
mysql_real_escape_string()
addslashes()
不同之处在于它知道连接字符集。因此它可以对服务器期望的字符集进行正确的转义。然而,到目前为止,客户端认为我们仍在使用latin1
进行连接,因为我们从未告诉过它。我们确实告诉 服务器 我们正在使用gbk
,但 客户端 仍然认为它是latin1
。因此,对
mysql_real_escape_string()
的调用会插入反斜杠,我们的“转义”内容中有一个自由悬挂的'
字符!事实上,如果我们查看$var
gbk
字符集中的 —,我们会看到:这正是攻击所需要的。
这部分只是一种形式,但这里是呈现的查询:
恭喜,您刚刚使用 PDO Prepared Statements 成功攻击了一个程序…
简单的修复
现在,值得注意的是,您可以通过禁用模拟的准备好的语句来防止这种情况:
这 通常会 导致一个真正的准备好的语句(即数据在一个单独的数据包中与查询一起发送)。但是,请注意 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 [基本多语言平面](http://en.wikipedia.org/wiki/Plane(Unicode)#Basic_Multilingual_Plane)。或者,您可以启用
NO_BACKSLASH_ESCAPES
SQL 模式,该模式(除其他外)会改变mysql_real_escape_string()
的操作。启用此模式后,0x27
将被替换为0x2727
而不是0x5c27
因此转义过程 无法 在任何易受攻击的编码中创建有效字符以前不存在(即0xbf27
仍然是0xbf27
等)—因此服务器仍将拒绝该字符串为无效。但是,请参阅 @eggyal 的回答,了解使用此 SQL 模式可能产生的不同漏洞(尽管不是使用 PDO)。安全示例
以下示例是安全的:
因为服务器期望
utf8
…因为我们已经正确设置了字符集,所以客户端和服务器匹配。
因为我们已经关闭了模拟的准备好的语句。
因为我们已经正确设置了字符集。
因为 MySQLi 一直在做真正的准备好的语句。
包起来
如果你:
或者
utf8
/latin1
/ascii
/ 等)或者
NO_BACKSLASH_ESCAPES
SQL模式你是 100% 安全的。
否则, 即使您使用 PDO 准备语句,您也很容易受到攻击……
附录
我一直在慢慢地研究一个补丁来改变默认不模拟为 PHP 的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这可能会导致问题(并且是测试失败的部分原因)。