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或其他可能的漏洞。

准备好的语句/参数化查询通常足以防止在该语句中注入第一个命令 * 。 如果你在应用程序的其他地方使用未经检查的dynamicsql,那么你仍然容易受到二阶注入的影响。

第二顺序注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以取消。 AFAIK,你几乎从来没有看到真正的工程二阶攻击,因为攻击者通常更容易进行社交工程,但是有时候由于额外的良性人物或类似的东西而出现二阶错误。

如果可以将值存储在稍后用作查询中的文字的数据库中,则可以完成二阶注入攻击。 作为一个例子,假设你在网站上创build一个帐户(假设这个问题的MySQL DB)时input以下信息作为新的用户名:

 ' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + ' 

如果对用户名没有其他限制,预准备语句仍然会确保上述embedded式查询在插入时不执行,并将值正确存储在数据库中。 但是,想象一下,应用程序稍后将从数据库中检索您的用户名,并使用string连接将该值包含一个新的查询。 您可能会看到别人的密码。 由于用户表中的前几个名称往往是pipe理员,您可能也刚刚放弃了农场。 (另请注意:这是不以纯文本存储密码的另一个原因!)

我们可以看到,准备好的语句对于单个查询是足够的,但是它们本身不足以防止整个应用程序中的SQL注入攻击,因为它们缺乏一种机制来强制应用程序内的所有对数据库的访问安全的代码。 但是,作为良好的应用程序devise的一部分 – 可能包括代码审查或静态分析等实践,或者使用限制dynamicSQL 准备语句的ORM,数据层或服务层是解决Sql注入的主要工具问题。 如果您遵循良好的应用程序devise原则,使您的数据访问与程序的其余部分分离,则可以轻松执行或审计每个查询正确使用参数化。 在这种情况下,SQL注入(一阶和二阶)都被完全阻止。


*事实certificate,在涉及宽字符时,MySql / PHP(好吧)只是愚蠢的处理参数,在这里还有一个罕见的例子,在这里可以允许注入通过参数化查询。

简短的回答是否定的 ,PDO准备不会为所有可能的SQL注入攻击辩护。 对于某些模糊的边缘情况。

我正在调整这个答案来谈论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)); 

在某些情况下,这将返回超过1行。 我们来分析一下这里发生了什么:

  1. select一个字符集

     $pdo->query('SET NAMES gbk'); 

    为了使这种攻击起作用,我们需要服务器在连接上预期的编码'如ASCII码0x27 ,最后一个字节是ASCII码,即0x5c 。 事实certificate,MySQL 5.6默认支持5种这样的编码: big5cp932gb2312gbksjis 。 我们将在这里selectgbk

    现在,在这里注意使用SET NAMES是非常重要的。 这将字符集设置为ON THE SERVER 。 还有另一种方式,但是我们很快就会到达那里。

  2. 有效载荷

    我们要用于这个注入的有效负载从字节序列0xbf27 。 在gbk ,这是一个无效的多字节字符; 在latin1 ,它是string¿' 。 请注意,在latin1 gbk0x27本身就是一个文字字符。

    我们select了这个有效载荷,因为如果我们在它上面调用addslashes() ,我们会在'字符之前插入一个ASCII \ ie 0x5c 。 所以,我们最后会得到0xbf5c27 ,它在gbk是一个两字符序列: 0xbf5c后面是0x27 。 或换句话说,一个有效的字符后跟一个非转义的' 。 但是我们不使用addslashes() 。 所以下一步…

  3. $ stmt->的execute()

    这里要实现的重要的一点是PDO在默认情况下不会做真正准备好的语句。 它模拟它们(用于MySQL)。 因此,PDO在内部build立查询string,对每个绑定string值调用mysql_real_escape_string() (MySQL C API函数)。

    调用mysql_real_escape_string()的C API不同于addslashes() ,因为它知道连接字符集。 所以它可以对服务器期望的字符集进行正确的转义。 然而,到目前为止,客户认为我们仍然在使用latin1来进行连接,因为我们从来没有告诉过它。 我们确实告诉我们使用gbk服务器 ,但客户端仍然认为它是latin1

    因此调用mysql_real_escape_string()插入反斜杠,我们在“转义”内容中有一个自由悬挂'字符! 事实上,如果我们在gbk字符集中查看$var ,我们会看到:

     缞'OR 1 = 1 / * 

    这正是攻击所需要的。

  4. 查询

    这部分只是一个forms,但这里是呈现的查询:

     SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1 

恭喜,您刚刚成功使用PDO准备好的语句攻击了一个程序…

简单的修复

现在,值得注意的是,您可以通过禁用模拟预处理语句来防止这种情况:

 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 

通常会导致一个真实的准备语句(即数据从查询单独的数据包中发送)。 但是请注意,PDO将悄无声息地回退到MySQL本身无法准备的模拟语句:那些可以在手册中列出的语句,但是要注意select适当的服务器版本)。

正确的修复

这里的问题是我们没有调用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之前并没有公开C API for mysql_set_charset() ,所以在以前的版本中,它不能阻止这个攻击的每一个可能的命令! 它现在被公开为一个DSN参数 ,应该用来代替 SET NAMES

拯救的恩典

正如我们在一开始所说的那样,要使这个攻击行得通,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4 不是脆弱的 ,但可以支持每一个 Unicode字符:所以你可以select使用它,但它只有自MySQL 5.5.3以来才可用。 另一种select是utf8 ,它也不易受攻击 ,可以支持整个Unicode Basic Multilingual Plane 。

或者,您可以启用NO_BACKSLASH_ESCAPES SQL模式,该模式会(除其他外)更改mysql_real_escape_string()的操作。 启用该模式后, 0x27将被replace为0x2727而不是0x5c27 ,因此转义过程不能在任何之前不存在的脆弱编码中创build有效字符(即0xbf27仍然是0xbf27等) – 所以服务器仍然会拒绝该string为无效。 但是,请参阅@ eggyal针对可能因使用此SQL模式(尽pipe不适用于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 /*")); 

因为我们已经closures了模拟的准备好的语句。

 $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)

要么

  • 不要使用易受攻击的字符集进行连接编码(只能使用utf8 / latin1 / ascii / etc)

要么

  • 启用NO_BACKSLASH_ESCAPES SQL模式

你100%安全。

否则, 即使您正在使用PDO准备好的语句 ,您也是易受攻击的

附录

我一直在缓慢地开发一个补丁来改变默认的模式,为未来版本的PHP做准备。 我遇到的问题是,当我这样做的时候,很多testing都会中断。 一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。 所以这可能会导致问题(并且是testingborking原因的一部分)。

不,他们并不总是。

这取决于您是否允许将用户input放入查询本身。 例如:

 $dbh = new PDO("blahblah"); $tableToUse = $_GET['userTable']; $stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username'); $stmt->execute( array(':username' => $_REQUEST['username']) ); 

将容易受到SQL注入的影响,并且在此示例中使用预准备语句将不起作用,因为用户input被用作标识符而不是数据。 这里的正确答案是使用某种过滤/validation,如:

 $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'); 

以上原因不起作用是因为DESCASC不是数据 。 PDO只能逃避数据 。 其次,你甚至不能把它引用。 允许用户selectsorting的唯一方法是手动筛选并检查它是DESC还是ASC

是的,这是足够的。 注入types攻击的方式是通过某种方式获得解释器(数据库)来评估某些东西,这应该是数据,就好像它是代码一样。 这是唯一可能的,如果你在同一个媒介中混合代码和数据(例如,当你构build一个查询作为一个string)。

参数化的查询通过分别发送代码和数据来工作,因此永远不可能find一个漏洞。

尽pipe如此,你仍然可能容易受到其他的注入式攻击。 例如,如果您使用HTML页面中的数据,则可能会受到XSStypes的攻击。

没有这是不够的(在一些特定情况下)! 默认情况下,使用MySQL作为数据库驱动程序时,PDO使用模拟的预处理语句。 在使用MySQL和PDO时,应该始终禁用模拟的预处理语句:

 $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 

另一件事总是应该做的是设置数据库的正确编码:

 $dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass'); 

也看到这个相关的问题: 我怎样才能防止SQL注入PHP中?

另外请注意,这只是关于显示数据时仍然需要注意的事情的数据库端。 例如,再次使用正确的编码和引用样式使用htmlspecialchars()

就个人而言,我总是会首先对数据运行某种forms的卫生设备,因为您永远不会相信用户input,但是当使用占位符/参数绑定时,input的数据将单独发送到服务器,然后绑定在一起。 这里的关键在于,它将提供的数据绑定到特定types和特定用途,并消除了更改SQL语句逻辑的任何机会。