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

会有一股神秘感。 提交于 2019-12-30 17:26:05

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

假设我有这样的代码:

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

不,这还不够(在某些特定情况下)! 默认情况下,当使用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()


#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类型的攻击。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!