提问



我们说我的代码是这样的:


$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );


PDO文件说:



  准备好的语句的参数不需要引用;驱动程序为您处理它。



这是否真的需要做些什么才能避免SQL注入?真的那么容易吗?


如果它有所作为,你可以假设MySQL。另外,我真的只是对使用针对SQL注入的预准备语句感到好奇。在这种情况下,我并不关心XSS或其他可能的漏洞。

最佳参考


简短的回答是 NO ,PDO准备不会保护您免受所有可能的SQL注入攻击。对于某些不起眼的边缘案例。


我正在调整这个答案来谈论PDO ......


长期答案并非如此简单。它基于此处展示的攻击。[86]


攻击



那么,让我们开始展示攻击......


$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. 选择字符集


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


    为了使这个攻击起作用,我们需要服务器在连接上期望编码'的编码,如ASCII,0x27 和,以使某些角色最终byte是一个ASCII \0x5c。事实证明,MySQL 5.6默认支持5种这样的编码:big5cp932gb2312gbksjis。我们在这里选择gbk


    现在,注意SET NAMES的使用非常重要。这会将字符集设置为服务器。还有另一种方法,但我们很快就会到达那里足够。

  2. 有效负载


    我们将用于此注入的有效负载以字节序列0xbf27开始。在gbk中,这是一个无效的多字节字符;在latin1中,它是字符串¿'。注意在latin1 gbk中,0x27本身就是一个字面'字符。


    我们选择了这个有效载荷,因为如果我们在它上面调用addslashes(),我们在'字符之前插入一个ASCII \0x5c。所以我们风最后0xbf5c27gbk中有两个字符序列:0xbf5c后跟0x27。或换句话说,一个有效的字符后跟一个未转义的'。但我们不使用addslashes()。所以下一步......

  3. $ stmt->执行()


    这里要认识到的重要一点是PDO默认情况下 NOT 执行真正准备好的语句。它模仿它们(对于MySQL)。因此,PDO在内部构建查询字符串,在每个绑定字符串值上调用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. 查询


    这部分只是一种形式,但这里是渲染的查询:


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



恭喜,您刚刚使用PDO Prepared Statements成功攻击了一个程序......


简单修复



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


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


这将通常导致真正准备好的语句(即,数据在来自查询的单独数据包中发送)。但是,要注意PDO将默默地回退到模仿MySQL无法本地准备的语句:可以在手册中列出的那些语句,但要注意选择适当的服务器版本。[87] [88]


正确修复



这里的问题是我们没有调用C API mysql_set_charset()而不是SET NAMES。如果我们这样做,如果我们从2006年开始使用MySQL版本,我们就会很好。


如果您正在使用早期的MySQL版本,则mysql_real_escape_string()中的错误意味着无效的多字节字符(例如我们的有效负载中的字符)被视为用于转义目的的单个字节即使客户端已被正确通知连接编码,所以这次攻击仍然会成功。这个错误在MySQL 4.1.20,5.0.22和5.1.11中得到修复。[89] [90] [91] [92]


但最糟糕的是PDO没有将mysql_set_charset()的C API公开到5.3.6,所以在以前的版本中,不能阻止每次可能命令的攻击!
 它现在作为DSN参数公开,应该使用而不是 SET NAMES ...... [93]


拯救恩典



正如我们在开始时所说的,为了使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4 不易受攻击并且可以支持每个 Unicode字符:所以你可以选择使用它而不是mdash;但它只能在MySQL 5.5.3之后才能使用。另一种选择是utf8,它也不易受攻击并且可以支持整个Unicode基本多语言平面。[94] [95] [96]


或者,您可以启用NO_BACKSLASH_ESCAPES SQL模式,其中(除其他外)改变mysql_real_escape_string()的操作。启用此模式后,0x27将替换为0x2727而不是0x5c27,因此转义过程无法在任何易受攻击的编码中创建有效字符以前不存在(即0xbf27仍然0xbf27等。)—所以服务器仍然会将字符串拒绝为无效。但是,请参阅@eggyal的答案,了解使用此SQL模式可能产生的其他漏洞(尽管不使用PDO)。[97]


安全示例



以下示例是安全的:


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等) AND PDO的DSN字符集参数(在PHP≥5.3.6中)







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







  • 启用NO_BACKSLASH_ESCAPES SQL模式



你100%安全。


否则,即使您正在使用PDO准备语句...... ,您也很容易受到攻击


附录



我一直在慢慢研究补丁,将默认设置更改为不能模拟为未来版本的PHP做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。因此,这可能会导致问题(并且是测试结果的一部分)。

其它参考1


准备好的语句/参数化查询通常足以阻止 * 上的第一顺序注入。如果在应用程序中的任何其他位置使用未经检查的动态sql,则仍然容易受到二阶注入的攻击。


二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现。 AFAIK,你几乎从来没有看到真正设计的二阶攻击,因为攻击者通常更容易进行社交工程,但你有时会因为额外的良性'字符或类似情况而出现二阶错误。


当您可以将值存储在稍后用作查询中的文字的数据库中时,您可以完成二阶注入攻击。例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设MySQL DB为此问题):


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


如果用户名没有其他限制,准备好的语句仍然可以确保上面的嵌入式查询在插入时不会执行,并将值正确地存储在数据库中。但是,想象一下以后应用程序会检索到你的来自数据库的用户名,并使用字符串连接将该值包含在新查询中。您可能会看到其他人的密码。由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了该服务器场。 (另请注意:这是不以明文形式存储密码的另一个原因!)


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





* 事实证明,当涉及到宽字符时,MySql/PHP对于处理参数只是愚蠢的,并且仍然存在罕见的案例在另一个高度投票的答案中,可以允许注入通过参数化查询。

其它参考2


不,他们并不总是。


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


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


上述原因不起作用的原因是DESCASC不是数据。 PDO只能转义为数据。其次,你甚至不能在它周围加上'引号。允许用户选择排序的唯一方法是手动过滤并检查它是DESC还是ASC

其它参考3


是的,这就足够了。注入类型攻击的工作方式是通过某种方式获得一个解释器(数据库)来评估一些应该是数据的东西,就好像它是代码一样。只有在同一介质中混合代码和数据时才可以这样做(例如,当您将查询构造为字符串时)。


参数化查询通过分别发送代码和数据来工作,因此永远不会在其中找到漏洞。


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

其它参考4


不,这还不够(在某些特定情况下)!默认情况下,当使用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');


另请参阅此相关问题:如何在PHP中阻止SQL注入?


另请注意,这只是关于数据库方面的事情,您在显示数据时仍需要注意自己。例如。通过使用htmlspecialchars()再次使用正确的编码和引用样式。

其它参考5


我个人总是首先对数据运行某种形式的卫生,因为你永远不能信任用户输入,但是当使用占位符/参数绑定时,输入的数据被单独发送到服务器到sql语句,然后绑定在一起。这里的关键是将提供的数据绑定到特定类型和特定用途,并消除任何更改SQL语句逻辑的机会。

其它参考6


Eaven如果你要阻止sql注入前端,使用html或js检查,你必须考虑前端检查是可绕过的。


您可以使用前端开发工具(现在内置firefox或chrome)来禁用js或编辑模式。


因此,为了防止SQL注入,在控制器内清理输入日期后端是正确的。


我建议您使用filter_input()本机PHP函数来清理GET和INPUT值。


如果您想继续使用安全性,对于合理的数据库查询,我建议您使用正则表达式来验证数据格式。
在这种情况下,preg_match()会帮助你!
但要小心!正则表达式引擎并不那么轻巧。仅在必要时使用,否则您的应用程序性能将下降。


安全性有成本,但不要浪费你的表现!


简单的例子:


如果你想仔细检查从GET收到的值是否是一个小于99的数字
    如果(!的preg_match( /[[0-9]] {1,2}/)){...}
比较重


if (isset($value) && intval($value)) <99) {...}


所以,最后的答案是:不!PDO Prepared Statements不会阻止所有类型的SQL注入;它不会阻止意外的值,只会意外连接