PDO准备好的语句是否足以防止SQL注入?


661

假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO文档说:

准备好的语句的参数不需要用引号引起来。司机为您处理。

那真的是我避免SQL注入所需要做的一切吗?真的那么容易吗?

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


5
更好的办法第七号答案stackoverflow.com/questions/134099/...
NullPoiиteя

Answers:


807

简短的回答是“ 否”,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. 选择字符集

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

    为了使这种攻击起作用,我们需要服务器在连接上期望的编码既要'以ASCII码编码0x27 也要具有某些字符的最终字节为ASCII码,\0x5c。事实证明,会默认在MySQL 5.6支持5个这样的编码:big5cp932gb2312gbksjis。我们将gbk在此处选择。

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

  2. 有效载荷

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

    我们选择此有效负载是因为,如果调用addslashes()它,我们会在字符之前插入一个ASCII码,\即。因此,我们将以结束,其中有两个字符序列:后跟。换句话说,就是一个有效字符,后跟一个未转义的。但是我们没有使用。继续下一步...0x5c'0xbf5c27gbk0xbf5c0x27'addslashes()

  3. $ stmt-> execute()

    这里要意识到的重要一点是,默认情况下,PDO 不会执行真正的预处理语句。它模拟它们(对于MySQL)。因此,PDO在内部构建查询字符串,并mysql_real_escape_string()在每个绑定的字符串值上调用(MySQL C API函数)。

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

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

    OR'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不能原生准备:那些可被在手册中,但要注意选择合适的服务器版本)。

正确的解决方法

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

如果您使用的是较早的MySQL版本,则存在一个错误mysql_real_escape_string()即出于逃避目的,无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节,即使已正确告知客户端连接编码,因此该攻击也可能仍然成功。该错误是固定在MySQL 4.1.205.0.225.1.11

但是最糟糕的是,直到5.3.6 PDO才公开C API mysql_set_charset(),因此在以前的版本中,它无法防止所有可能的命令遭受这种攻击!现在,它作为DSN参数公开,应代替 SET NAMES ...

拯救恩典

正如我们在一开始所说的那样,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4不容易,但可以支持所有的 Unicode字符:所以你可以选择使用的是代替,但它只是可利用从MySQL 5.5.3。另一种选择是utf8,它也不易受到攻击,并且可以支持整个Unicode Basic Multilingual Plane

或者,您可以启用NO_BACKSLASH_ESCAPESSQL模式,该模式(除其他外)会更改的操作mysql_real_escape_string()。启用该模式,0x27将被替换0x2727,而不是0x5c27从而逃逸过程不能在任何地方,他们以前不存在的脆弱编码的创建有效的字符(即0xbf270xbf27等) -这样的服务器仍然会拒绝的字符串为无效。但是,请参阅@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始终会执行真正的预备语句。

包起来

如果你:

  • 使用MySQL的现代版本(5.1版,所有5.5版,5.6版等) PDO的DSN字符集参数(在PHP≥5.3.6中)

要么

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

要么

  • 启用NO_BACKSLASH_ESCAPESSQL模式

您是100%安全的。

否则,即使您使用的是PDO预准备语句,也容易受到攻击

附录

我一直在缓慢地开发一个补丁程序,以更改默认值,以不模仿将来的PHP版本。我遇到的问题是,当我这样做时,很多测试都失败了。一个问题是,模拟的Prepare将仅在执行时抛出语法错误,而真正的Prepare将在Prepare上抛出错误。这样可能会导致问题(这是测试很乏味的部分原因)。


47
这是我发现的最佳答案。.您能提供一个链接以获取更多参考吗?
StaticVariable 2012年

1
@nicogawenda:那是一个不同的错误。在5.0.22之前的版本中,mysql_real_escape_string无法正确处理将连接正确设置为BIG5 / GBK的情况。因此,实际上甚至调用mysql_set_charset()mysql <5.0.22都会受到此bug的攻击!所以不,这篇文章仍然适用于5.0.22(因为mysql_real_escape_string仅可用于呼叫from mysql_set_charset(),这是这篇文章所说的绕过的调用)...
ircmaxell 2013年

1
@progfa无论是否这样做,您都应该在对用户数据执行任何操作之前始终验证服务器上的输入。
Tek

2
请注意,这NO_BACKSLASH_ESCAPES还会引入新的漏洞:stackoverflow.com/a/23277864/1014813
lepix

2
@slevin中的“ OR 1 = 1”是您想要的占位符。是的,它正在搜索名称中的值,但是假设“ OR 1 = 1”部分是“ UNION SELECT * FROM users”。现在,您可以控制查询,这样就可以滥用它……
ircmaxell

515

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

2阶注入意味着数据在包含在查询中之前已经在数据库中循环了一次,并且很难提取。AFAIK,您几乎永远不会看到真正的工程化二阶攻击,因为攻击者通常更容易进行社交工程化,但是由于额外的良性'角色或类似因素,有时会出现二阶错误。

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

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

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

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


*事实证明,当涉及到宽字符时,MySql / PHP只是(愚蠢)只是在处理参数方面很愚蠢,在这里另一个极受好评的答案中仍然概述了一种罕见的情况,这种情况可以允许注入通过参数化来进行查询。


6
那很有意思。我不知道一阶还是二阶。您能否详细说明二阶的工作原理?
Mark Biek

193
如果所有查询都已参数化,则还可以防止二阶注入。一阶注入忘记了用户数据是不可信的。二阶注入忘记了数据库数据是不可信的(因为它最初来自用户)。
cjm

6
谢谢cjm。我还发现这篇文章有助于解释二阶注入: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek

49
是的。但是三阶注入呢?必须意识到这些。
troelskn 2011年

81
@troelskn一定是开发人员成为不可信任数据来源的地方
MikeMurko

45

不,并非总是如此。

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

$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只能逸出数据。其次,您甚至不能在'引号周围加上引号。只有这样,才能允许使用者选择的排序是手动过滤器,并检查它要么DESCASC


11
我是否在这里遗漏了一些东西,但是不是准备好的语句的全部目的是为了避免像字符串一样对待sql吗?不会像$ dbh-> prepare('SELECT * FROM:tableToUse where username =:username'); 解决您的问题?
罗伯·福雷斯特

4
@RobForrest是的,您不见了:)。您绑定的数据仅适用于DDL(数据定义语言)。您需要这些引号和适当的转义。在查询的其他部分放置引号很有可能使查询中断。例如,SELECT * FROM 'table'应该是错误的SELECT * FROM `table`或没有任何背景。然后,像一些事情ORDER BY DESC,其中DESC来自用户不能简单地逃脱。因此,实际情况是无限的。
2012年

8
我不知道有6个人会如何反对提议对准备好的陈述的明显错误使用的评论。即使他们曾经尝试过一次,他们也立即发现使用命名参数代替表名是行不通的。
费利克斯·加侬-格雷尼尔

如果您想学习PDO,这是一个很棒的PDO教程。a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN

11
您永远不要使用查询字符串/ POST正文来选择要使用的表。如果您没有模型,请至少使用a switch派生表名。
ZiggyTheHamster 2014年

29

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

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

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


10
“从不”是夸大其词的方式,以至于造成误导。如果您错误地使用了准备好的语句,那总比根本不使用它们更好。(当然,向其中注入了用户输入的“预备语句”不能达到目的……但我实际上已经看到了它的实现。预备语句不能将标识符(表名等)作为参数来处理。)为此,一些PDO驱动程序模拟了准备好的语句,并且它们仍有可能错误地执行此操作(例如,通过半自信地解析SQL)。简短版:永远不要以为是那么容易。
cHao 2012年

29

不,这还不够(在某些特定情况下)!默认情况下,当使用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()以正确的编码和引用样式再次使用。


14

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


1

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

您可以禁用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预处理语句不能阻止所有类型的sql注入”。它不会阻止意外的值,只是防止意外的串联


5
您将SQL注入与其他东西混淆了,这使您的答案完全不相关
您的常识
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.