711

假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO 文档说:

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

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

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

4

7 回答 7

889

简短的回答是否定的,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 ie 0x27 又要有一些字符,其最后一个字节是 ASCII \ie 0x5c。事实证明,MySQL 5.6 默认支持 5 种这样的编码:big5cp932gb2312和. 我们会在这里选择。gbksjisgbk

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

  2. 有效载荷

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

    我们选择了这个payload,因为如果我们调用addslashes()它,我们会在字符之前插入一个ASCII \ie 。所以我们最终会得到,它是一个两个字符的序列:后跟. 或者换句话说,一个有效字符后跟一个未转义的. 但我们没有使用. 那么进行下一步...0x5c'0xbf5c27gbk0xbf5c0x27'addslashes()

  3. $stmt->执行()

    这里要意识到的重要一点是,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 /*

    这正是攻击所需要的。

  4. 查询

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

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

恭喜,您刚刚使用 PDO Prepared Statements 成功攻击了一个程序...

简单的修复

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

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

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

正确的修复

这里的问题是我们没有调用 C APImysql_set_charset()而不是SET NAMES. 如果我们这样做了,只要我们使用自 2006 年以来的 MySQL 版本就可以了。

如果您使用的是较早的 MySQL 版本,那么一个错误意味着即使客户端已正确通知连接编码,也会将无效的多字节字符(例如我们的有效负载中mysql_real_escape_string()的那些字符)视为单字节以进行转义,因此这种攻击会仍然成功。该错误已在 MySQL 4.1.20、5.0.225.1.11修复

但最糟糕的部分是直到 5.3.6PDO才公开 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///等latin1ascii

或者

  • 启用NO_BACKSLASH_ESCAPESSQL 模式

你是 100% 安全的。

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

附录

我一直在慢慢地研究一个补丁来改变默认不模拟为 PHP 的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这可能会导致问题(并且是测试失败的部分原因)。

于 2012-08-30T17:22:04.327 回答
521

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

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

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

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

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

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


*事实证明,当涉及到宽字符时,MySql/PHP (好吧,曾经)只是在处理参数方面很愚蠢,并且在此处其他高度投票的答案中仍然概述了一种罕见的情况,它可以允许注入通过参数化询问。

于 2008-09-25T15:50:51.960 回答
46

不,它们并非总是如此。

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

$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');

以上不起作用的原因是因为DESCand ASCare not data。PDO 只能对数据进行转义。其次,你甚至不能'在它周围加上引号。允许用户选择排序的唯一方法是手动过滤并检查它是否为DESCASC

于 2010-04-21T09:00:06.190 回答
29

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

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

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

于 2008-09-25T15:55:46.533 回答
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()再次使用正确的编码和引用样式。

于 2012-08-30T17:00:15.760 回答
14

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

于 2008-09-25T15:50:43.770 回答
0

即使您要防止 sql 注入前端,使用 html 或 js 检查,您也必须考虑前端检查是“可绕过的”。

您可以禁用 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 Prepared Statements 不会阻止所有类型的 sql 注入”;它不会阻止意外的值,只是意外的连接

于 2018-03-04T20:17:56.390 回答