当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《PDO准备好的语句是否足以阻止SQL注入?》经验,为你挑选了6个好方法。

假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO文件说:

准备语句的参数不需要引用; 司机为你处理.

这真的是我需要做的就是避免SQL注入吗?这真的很容易吗?

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



1> ircmaxell..:

简短的回答是NO,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,gbksjis.我们会gbk在这里选择.

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

    有效载荷

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

    我们选择了这个有效载荷,因为,如果我们叫addslashes()上他,我们就插入一个ASCII \0x5c,在之前'的字符.所以我们结束了0xbf5c27,这gbk是一个两个字符的序列:0xbf5c接下来0x27.或者换句话说,一个有效的字符后跟一个未转义的字符'.但我们没有使用addslashes().那么下一步......

    $ 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 1=1 /*

    这正是攻击所需要的.

    查询

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

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

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

简单修复

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

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

但最糟糕的是,直到5.3.6 PDO才公开C API mysql_set_charset(),因此在以前的版本中,它无法阻止每次可能命令的攻击!它现在作为DSN参数公开,应该使用它代替 SET NAMES ...

拯救恩典

正如我们在开始时所说的,为了使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码. utf8mb4不容易,但可以支持所有的 Unicode字符:所以你可以选择使用的是代替,但它只是从MySQL 5.5.3可用.另一种选择是utf8,它也不易受攻击,可以支持整个Unicode 基本多语言平面.

或者,您可以启用NO_BACKSLASH_ESCAPESSQL模式,其中(除其他外)改变了操作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一直都做真实的准备语句.

包起来

如果你:

使用MySQL的现代版本(后期5.1,全部5.5,5.6等) PDO的DSN字符集参数(在PHP≥5.3.6中)

要么

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

要么

启用NO_BACKSLASH_ESCAPESSQL模式

你100%安全.

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

附录

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


这是我找到的最佳答案..你能提供更多参考链接吗?
请注意,`NO_BACKSLASH_ESCAPES`也可能引入新的漏洞:http://stackoverflow.com/a/23277864/1014813
@slevin"OR 1 = 1"是占位符,无论你想要什么.是的,它正在搜索名称中的值,但想象"OR 1 = 1"部分是"UNION SELECT*FROM users".您现在控制查询,因此可以滥用它...

2> Joel Coehoor..:

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

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

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

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

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

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


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


如果所有查询都是参数化的,那么您也可以防止二阶注入.一阶注入忘记了用户数据不值得信任.二阶注入忘记了数据库数据是不值得信任的(因为它最初来自用户).
@troelskn必须是开发人员不可靠数据来源的地方
没错.但是**三阶注射怎么样**.必须要意识到这些.
那很有意思.我不知道第一顺序和第二顺序.你能详细说明二阶如何运作吗?
谢谢你.我还发现本文有助于解释二阶注射:http://www.codeproject.com/KB/database/SqlInjectionAttacks.aspx
我不知道订单是什么.读到这个我以为_第三和第四,甚至更多第五?第六个必定是如此致命......
是的,但默认情况下PDO不使用真实的准备语句;-).请参阅下面的答案,了解攻击的演示和解释......
@Waqas然后你有各种各样的问题.如果这些标签有效且需要存储在数据库中怎么办?为什么在*real_escape之后使用reg exp*?在*验证通过之前,用户输入不应达到DB sanitisation*.你为什么要做mysqli_real_escape_string`然后再做PDO?当然,你的最终PDO使其安全*如果*你使用参数化语句,但你添加的所有额外的(无意义的)"安全性"更有可能引起数据库安全性的安全漏洞,或者至少是噩梦很难理解和使用的代码.

3> Tower..:

不,他们并不总是.

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

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


我在这里遗漏了一些东西但是不是准备好的语句的全部要点,以避免像对待字符串一样处理sql?不会像$ dbh-> prepare('SELECT*FROM:tableToUse where username =:username'); 解决你的问题?
您永远不应该使用查询字符串/ POST正文来选择要使用的表.如果你没有模型,至少使用`switch`来派生表名.
我想知道有6个人可以提出一个评论,建议明确错误地使用预先准备好的陈述.如果他们甚至尝试了一次,他们就会立即发现使用命名参数代替表名将无效.
@RobForrest是的,你错过了:).您绑定的数据仅适用于DDL(数据定义语言).你需要那些引用和适当的转义.为查询的其他部分放置引号会很有可能破坏它.例如,`SELECT*FROM'table'`可能是错误的,因为它应该是`SELECT*FROM \`table \``或没有任何后退.然后,诸如"ORDER BY DESC"之类的东西,其中来自用户的"DESC"不能简单地被转义.因此,实际情况相当无限.

4> troelskn..:

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

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

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


"永远"是**方式**夸大它,到了误导的程度.如果您错误地使用预准备语句,那么根本不使用它们就好多了.(当然,注入了用户输入的"准备好的声明"会破坏目的......但实际上我已经看过它了.准备好的语句不能将标识符(表名等)作为参数处理.)为此,一些PDO驱动程序*模拟*预处理语句,并且它们有空间不正确地执行(例如,通过半分析SQL).简短版本:从不认为它很容易.

5> PeeHaa..:

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



6> JimmyJ..:

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

推荐阅读
周扒pi
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有