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

249478 次浏览

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

准备好的语句/参数化查询足以防止SQL注入,但只有在应用程序中的每个查询都始终使用时。

如果您在应用程序的任何其他地方使用未经检查的动态SQL,它仍然容易受到第二秩序注入的攻击。

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

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

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

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

然后我们看到,如果预准备语句仅用于单个查询,而忽略了所有其他查询,那么这个查询没有足以防止整个应用程序中的sql注入攻击,因为它们缺乏一种机制来强制使用安全代码对应用程序中的数据库进行所有访问。但是,用作良好应用程序设计的一部分-这可能包括代码审查或静态分析等实践,或使用限制动态sql的ORM、数据层或服务层-**准备好的发言稿解决SQL注入问题的主要工具。**如果你遵循良好的应用程序设计原则,这样你的数据访问就与程序的其余部分分开,那么强制执行或审计每个查询正确使用参数化就变得容易了。在这种情况下,完全防止了sql注入(第一顺序和第二顺序)。


*事实证明,MySql/PHP(很久很久以前)只是在涉及宽字符时处理参数时愚蠢,并且在其他高度投票的答案中概述了罕见的情况

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

参数化查询通过单独发送代码和数据来工作,因此从未可以找到其中的漏洞。

但是,您仍然容易受到其他注入型攻击。例如,如果您在超文本标记语言页面中使用数据,您可能会受到XSS类型攻击。

不,他们并不总是。

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

$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

不,这还不够(在某些特定情况下)!默认情况下,PDO在使用MySQL作为数据库驱动程序时使用模拟准备语句。在使用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注入?

请注意,这只会保护您免受SQL注入,但您的应用程序仍然容易受到其他类型的攻击。例如,您可以通过再次使用具有正确编码和引用样式的htmlspecialchars()来防止XSS。

简短的答案是,如果使用得当,PDO准备是足够安全的。


我正在改编这个答案来谈论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中编码',即0x270x270有一些字符,其最后一个字节是ASCII\,即0x5c。事实证明,MySQL 5.6默认支持5种这样的编码:big5cp932gb2312gbksjis。我们将在这里选择gbk

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

  2. 有效载荷

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

    我们选择这个有效负载是因为,如果我们在其上调用addslashes(),我们将在'字符之前插入一个ASCII\,即0x5c。所以我们最终得到0xbf5c27,在gbk中是一个两个字符的序列:0xbf5c后跟0x27。或者换句话说,一个\0字符后跟一个未转义的'。但我们没有使用addslashes()。所以下一步…

  3. #stmt-执行

    这里要意识到的重要一点是,PDO默认情况下做不是做真正的准备语句。它模拟它们(对于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 /*

    Which is exactly what the attack requires.

  4. The Query

    This part is just a formality, but here's the rendered query:

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

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

简单的修复

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

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

这将通常导致真正的准备语句(即数据在查询的单独数据包中发送)。但是,请注意,PDO会静默后备来模拟MySQL无法本地准备的语句:它可以在手册中为列出,但要注意选择适当的服务器版本)。

正确的修复

这里的问题是我们使用SET NAMES而不是C API的mysql_set_charset()。否则,攻击不会成功。但最糟糕的是,PDO直到5.3.6才公开mysql_set_charset()的C API,所以在之前的版本中,它不能阻止了所有可能的命令的这种攻击! 它现在暴露为DSN参数,应该使用而不是SET NAMES

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");

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

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

  • 启用NO_BACKSLASH_ESCAPESSQL模式

你是100%安全的。

否则,你很脆弱即使您使用的是PDO准备语句…

增编

我一直在慢慢地制作一个补丁,将默认值更改为不为PHP的未来版本模拟准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这会导致问题(也是测试无聊的部分原因)。

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

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

因此,为了防止SQL注入,对控制器内的输入日期后端进行消毒是正确的。

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

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

安全是有成本的,但不要浪费你的业绩!

简单的例子:

如果您想再次检查从GET接收到的值是否是一个数字,小于99 if(!preg_match('/[0-9]{1,2}/')){…} #36825;重

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

所以,最终的答案是:“不!PDO准备好的语句不会阻止所有类型的sql注入”;它不会阻止意外的值,只是意外的连接