[英]Are PDO prepared statements sufficient to prevent SQL injection?
假设我有这样的代码:
$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 = "\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
并具有某些字符,其最终字节是 ASCII \\
即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 Prepared Statements 的程序......
现在,值得注意的是,您可以通过禁用模拟准备好的语句来防止这种情况:
$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 基本多语言平面。
或者,您可以启用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("\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 一直在做真正的准备好的语句。
如果你:
或者
utf8
/ latin1
/ ascii
/ 等)或者
NO_BACKSLASH_ESCAPES
SQL 模式你是 100% 安全的。
否则,即使您使用 PDO 准备好的语句,您也很容易受到攻击......
我一直在慢慢研究一个补丁,将默认设置更改为不模拟为未来版本的 PHP 做准备。 我遇到的问题是,当我这样做时,很多测试都会中断。 一个问题是模拟准备只会在执行时抛出语法错误,而真正的准备会在准备时抛出错误。 所以这可能会导致问题(并且是测试失败的部分原因)。
准备好的语句/参数化查询通常足以防止对该语句的一阶注入* 。 如果您在应用程序的其他任何地方使用未经检查的动态 sql,您仍然容易受到二阶注入的影响。
二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难实现。 AFAIK,您几乎从未见过真正经过工程设计的二阶攻击,因为攻击者通常更容易通过社会工程进入,但有时您会因为额外的良性'
字符或类似字符而出现二阶错误。
当您可以将一个值存储在数据库中,然后在查询中用作文字时,您就可以完成二阶注入攻击。 例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设此问题使用 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
。
不,这还不够(在某些特定情况下)! 默认情况下,当使用 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()
。
是的,这就足够了。 注入类型攻击的工作方式是以某种方式让解释器(数据库)评估某些东西,它应该是数据,就好像它是代码一样。 这只有在您将代码和数据混合在同一介质中时才有可能(例如,当您将查询构造为字符串时)。
参数化查询通过分别发送代码和数据来工作,因此永远不可能在其中找到漏洞。
但是,您仍然可能容易受到其他注入式攻击。 例如,如果您在 HTML 页面中使用数据,您可能会受到 XSS 类型的攻击。
就我个人而言,我总是首先对数据运行某种形式的卫生,因为您永远不能相信用户输入,但是当使用占位符/参数绑定时,输入的数据会分别发送到服务器的 sql 语句,然后绑定在一起。 这里的关键是,这将提供的数据绑定到特定类型和特定用途,并消除了更改 SQL 语句逻辑的任何机会。
即使您要防止 sql 注入前端,使用 html 或 js 检查,您必须考虑前端检查是“可绕过的”。
您可以禁用 js 或使用前端开发工具(现在内置 firefox 或 chrome)编辑模式。
因此,为了防止 SQL 注入,清理控制器内的输入日期后端是正确的。
我想建议您使用 filter_input() 原生 PHP 函数来清理 GET 和 INPUT 值。
如果您想继续安全,对于明智的数据库查询,我建议您使用正则表达式来验证数据格式。 在这种情况下,preg_match() 会帮助你! 但要小心! 正则表达式引擎不是那么轻。 仅在必要时使用它,否则您的应用程序性能将下降。
安全是有代价的,但不要浪费你的性能!
简单的例子:
如果你想仔细检查从 GET 收到的值是否是一个数字,小于 99 if(!preg_match('/[0-9]{1,2}/')){...} 是更重的
if (isset($value) && intval($value)) <99) {...}
所以,最终的答案是:“不!PDO Prepared Statements 并不能阻止所有类型的 sql 注入”; 它不会阻止意外的值,只是意外的串联
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.