PDO准备的语句是否足以防止SQL注入?
假设我有这样的代码:
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDO文件说:
准备报表的参数不需要引用; 司机为你处理它。
这是我真正需要做的,以避免SQL注入? 这真的很简单吗?
如果它有所作为,你可以假设MySQL。 另外,我真的只对使用SQL注入准备语句感到好奇。 在这种情况下,我不关心XSS或其他可能的漏洞。
简短的答案是否定的 ,PDO准备不会为所有可能的SQL注入攻击辩护。 对于某些模糊的边缘情况。
我正在调整这个答案来谈论PDO ......
长的回答并不容易。 它基于在这里演示的攻击。
攻击
所以,让我们以显示攻击开始......
$pdo->query('SET NAMES gbk');
$var = "xbfx27 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
并且有一些字符的最终字节为ASCII ie
0x5c
。 事实证明,默认情况下MySQL 5.6支持5种编码: big5
, cp932
, gb2312
, gbk
和sjis
。 我们在这里选择gbk
。
现在,在这里注意使用SET NAMES
非常重要。 这将字符集设置为ON THE SERVER 。 还有另一种方式,但我们很快就会到达那里。
有效载荷
我们将用于此注入的有效负载从字节序列0xbf27
。 在gbk
,这是一个无效的多字节字符; 在latin1
,它是字符串¿'
。 请注意,在latin1
和 gbk
, 0x27
本身就是一个文字'
字符。
我们选择了这个有效载荷,因为如果我们在它上面调用addslashes()
,我们会在'
字符之前插入一个ASCII ie
0x5c
。 因此,我们最终会得到0xbf5c27
,它在gbk
是一个两字符序列: 0xbf5c
后面是0x27
。 或者换句话说,一个有效的字符后跟一个非转义的'
。 但是我们没有使用addslashes()
。 所以下一步...
$ stmt->的execute()
在这里实现的重要一点是,PDO在默认情况下并没有做真正准备好的语句。 它模拟它们(用于MySQL)。 因此,PDO内部构建查询字符串,并在每个绑定字符串值上调用mysql_real_escape_string()
(MySQL C API函数)。
对mysql_real_escape_string()
的C API调用与addslashes()
mysql_real_escape_string()
不同之处在于它知道连接字符集。 所以它可以对服务器期望的字符集进行适当的转义。 但是,到目前为止,客户认为我们仍然在使用latin1
来进行连接,因为我们从来没有告诉过它。 我们确实告诉我们使用gbk
的服务器,但客户仍认为它是latin1
。
因此,调用mysql_real_escape_string()
将插入反斜杠,并且在我们的“转义”内容中有一个自由悬挂'
字符! 实际上,如果我们要查看gbk
字符集中的$var
,我们会看到:
縗' OR 1=1 /*
这正是攻击所需要的。
查询
这部分只是一个形式,但是这里是呈现的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚使用PDO准备好的语句成功攻击了一个程序...
简单的修复
现在,值得注意的是,您可以通过禁用模拟预处理语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这通常会导致一个真实的准备好的语句(即数据从查询中以单独的数据包发送)。 但是,请注意,PDO将默默回退到模拟MySQL无法自行准备的语句:可以在手册中列出的语句,但要注意选择适当的服务器版本)。
正确的修复
这里的问题是我们没有调用C API的mysql_set_charset()
而不是SET NAMES
。 如果我们这样做了,我们会很好,只要我们自2006年以来使用MySQL版本。
如果您使用的是早期的MySQL版本,那么mysql_real_escape_string()
的错误意味着无效的多字节字符(如我们的有效内容中的那些字符)被视为单个字节以用于转义目的,即使客户端已正确通知连接编码等这次攻击仍然会成功。 该错误在MySQL 4.1.20,5.0.22和5.1.11中修复。
但最糟糕的部分是, PDO
在5.3.6之前没有将mysql_set_charset()
的C API公开,所以在以前的版本中,它无法阻止这种攻击的每一个可能的命令! 它现在作为DSN参数公开,应该使用它来代替 SET NAMES
...
节省的恩典
正如我们在一开始所说的那样,要使这种攻击行得通,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4
不易受攻击,但可以支持每个Unicode字符:所以你可以选择使用它 - 但它只有在MySQL 5.5.3以后才可用。 另一种选择是utf8
,它也不易受攻击,并且可以支持整个Unicode Basic Multilingual Plane。
或者,您可以启用NO_BACKSLASH_ESCAPES
SQL模式,该模式会(除其他外)更改mysql_real_escape_string()
的操作。 启用该模式后, 0x27
将替换为0x2727
而不是0x5c27
,因此转义进程无法在之前不存在的任何易受攻击的编码(即0xbf27
仍为0xbf27
等)中创建有效字符 - 因此服务器仍然会拒绝该字符串为无效。 但是,请参阅@ eggyal针对使用此SQL模式可能出现的不同漏洞的答案(尽管不适用于PDO)。
安全示例
以下例子是安全的:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("xbfx27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
因为服务器期待utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("xbfx27 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("xbfx27 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("xbfx27 OR 1=1 /*"));
因为我们已经正确设置了字符集。
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "xbfx27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
因为MySQLi一直都在准备真正的语句。
包起来
如果你:
要么
utf8
/ latin1
/ ascii
/ etc) 要么
NO_BACKSLASH_ESCAPES
SQL模式 你100%安全。
否则, 即使您正在使用PDO准备好的语句 ,您也很容易受到攻击......
附录
我一直在缓慢地研究一个补丁,将默认设置改为不为未来版本的PHP做准备。 我遇到的问题是,当我这样做时,很多测试都会中断。 一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。 所以这可能会导致问题(并且是测试borking原因的一部分)。
准备好的陈述/参数化查询通常足以防止该陈述中的第一次订单注入*。 如果您在应用程序的其他任何位置使用未检查的动态SQL,则仍然容易受到二阶注入的影响。
二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现。 据我所知,你几乎从来没有看到真正的工程二阶攻击,因为它是攻击者通常更容易的社会工程师他们的方式,但你有时不得不二阶错误裁剪,因为额外的良性达'
的字符或相似。
如果可以使值存储在稍后用作查询中的文字的数据库中,则可以完成二阶注入攻击。 作为一个例子,假设您在网站上创建帐户时输入以下信息作为新用户名(假设此问题为MySQL DB):
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
如果用户名没有其他限制,则预准备语句仍然会确保上述嵌入式查询在插入时不会执行,并将值正确存储在数据库中。 但是,想象一下,应用程序稍后将从数据库中检索用户名,并使用字符串连接将该值包含一个新查询。 您可能会看到其他人的密码。 由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了农场。 (另请注意:这是不以纯文本存储密码的另一个原因!)
然后,我们看到,准备好的语句对于单个查询已经足够了,但是它们本身并不足以防止整个应用程序中的SQL注入攻击,因为它们缺乏一种机制来强制在应用程序中使用对数据库的所有访问安全代码。 但是,作为良好应用程序设计的一部分 - 可能包括代码审阅或静态分析等实践,或者使用限制动态SQL 准备语句的ORM,数据层或服务层,这些是解决Sql注入的主要工具问题。 如果您遵循良好的应用程序设计原则,使您的数据访问与程序的其余部分分离,则可以轻松执行或审计每个查询是否正确使用参数化。 在这种情况下,SQL注入(一阶和二阶)都被完全阻止。
*事实证明,当涉及宽字符时,MySql / PHP(好吧)只是愚蠢的处理参数,而且在这里还有一个罕见的情况,在这里可以允许注入通过参数化查询。
不,他们并不总是。
这取决于您是否允许将用户输入放入查询本身。 例如:
$dbh = new PDO("blahblah");
$tableToUse = $_GET['userTable'];
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
将容易受到SQL注入的影响,并且在本示例中使用预准备语句将不起作用,因为用户输入被用作标识符而不是数据。 这里的正确答案是使用某种过滤/验证,如:
$dbh = new PDO("blahblah");
$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))
$tableToUse = 'users';
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
注意:您不能使用PDO绑定DDL(数据定义语言)以外的数据,即这不起作用:
$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');
上述不起作用的原因是因为DESC
和ASC
不是数据。 PDO只能逃避数据。 其次,你甚至不能把'
周围的引号。 允许用户选择排序的唯一方法是手动筛选并检查它是DESC
还是ASC
。
上一篇: Are PDO prepared statements sufficient to prevent SQL injection?