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

假设我有这样的代码: php

$dbh = new PDO("blahblah");

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

PDO文档说: html

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

那真的是我避免SQL注入所须要作的一切吗? 真的那么容易吗? git

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


#1楼

不,这还不够(在某些特定状况下)! 默认状况下,当使用MySQL做为数据库驱动程序时,PDO使用模拟的准备好的语句。 使用MySQL和PDO时,应始终禁用模拟的准备好的语句: sql

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

老是应该作的另外一件事是它设置数据库的正确编码: 数据库

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

另请参阅如下相关问题: 如何防止PHP中的SQL注入? c#

还要注意,这仅与数据库方面有关,在显示数据时您仍然须要注意本身。 例如,以正确的编码和引用样式再次使用htmlspecialchars()安全


#2楼

简短的回答是“ 否” ,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 。 所以,咱们将得到0xbf5c27 ,它在gbk是两个字符序列: 0xbf5c后跟0x27 。 换句话说,就是一个有效字符,后跟一个未转义的' 。 可是咱们没有使用addslashes() 。 继续下一步...

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

可是最糟糕的是, PDO直到5.3.6才公开mysql_set_charset()的C API,所以在之前的版本中,它没法针对全部可能的命令阻止这种攻击! 如今它做为DSN参数公开,应该代替 SET NAMES

拯救的恩典

正如咱们在一开始所说的那样,要使这种攻击起做用,必须使用易受攻击的字符集对数据库链接进行编码。 utf8mb4 并不是易受攻击 ,但能够支持每一个 Unicode字符:所以您能够选择使用它,可是它仅自MySQL 5.5.3起可用。 utf8是一种替代方法,它也不易受攻击 ,能够支持整个Unicode Basic Multilingual Plane

另外,您能够启用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始终会执行真正的预备语句。

包起来

若是你:

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

要么

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

要么

  • 启用NO_BACKSLASH_ESCAPES SQL模式

您是100%安全的。

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

附录

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


#3楼

就我的而言,我老是会首先对数据进行某种形式的卫生处理,由于您永远不会信任用户输入,可是当使用占位符/参数绑定时,输入的数据将分别发送到服务器的sql语句,而后绑定在一块儿。 此处的关键是,这会将提供的数据绑定到特定类型和特定用途,并消除了更改SQL语句逻辑的任何机会。


#4楼

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

2阶注入意味着数据在包含在查询中以前已经在数据库中循环了一次,而且很难提取。 据我所知,你几乎历来没有看到真正的工程二阶攻击,由于它是攻击者的社会工程师他们的方式一般更容易,但你有时有2次错误裁剪,由于额外的良性达'的字符或类似。

当您可使一个值存储在数据库中,该数据库之后用做查询中的文字时,就能够完成二阶注入攻击。 举例来讲,假设您在网站上建立账户时输入如下信息做为新的用户名(假设使用MySQL DB解决此问题):

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

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

咱们看到,那么,准备语句是足以让一个单一的查询,但它们自己并不足以防止SQL注入攻击贯穿整个应用程序,由于他们缺少一种机制,以执行该应用程序使用中的全部对数据库的访问安全代码。 可是,用做良好应用程序设计的一部分(其中可能包括诸如代码审查或静态分析之类的实践,或者使用限制动态sql的ORM,数据层或服务层), 准备好的语句 解决Sql Injection的主要工具问题。 若是您遵循良好的应用程序设计原则,从而将数据访问与程序的其他部分分开,则能够轻松实施或审核每一个查询正确使用参数化的过程。 在这种状况下,彻底防止了sql注入(一阶和二阶)。


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


#5楼

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

参数化查询经过分别发送代码和数据来工做,所以永远不可能在其中发现漏洞。

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

相关文章
相关标签/搜索