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

Are PDO prepared statements sufficient to prevent SQL injection?

提问人:Mark Biek 提问时间:9/25/2008 最后编辑:Patrick HofmanMark Biek 更新时间:9/29/2022 访问量:258677

问:

假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO文档说:

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

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

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

php 安全 pdo sql 注入

评论


答:

15赞 JimmyJ 9/25/2008 #1

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

523赞 Joel Coehoorn 9/25/2008 #2

准备好的语句/参数化查询足以防止 SQL 注入,但前提是要始终使用,用于应用程序中的每个查询。

如果在应用程序中的任何其他位置使用未经检查的动态 SQL,它仍然容易受到二阶注入的影响。

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

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

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

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

因此,我们看到,如果预准备语句仅用于单个查询,而忽略了所有其他查询,则这一个查询不足以防止整个应用程序中的 sql 注入攻击,因为它们缺乏一种机制来强制使用安全代码对应用程序内数据库的所有访问。但是,作为良好应用程序设计的一部分(可能包括代码审查或静态分析等实践,或使用限制动态 sql 的 ORM、数据层或服务层),预准备语句解决 SQL 注入问题的主要工具。 强制执行或审核每个查询是否正确使用参数化变得容易。在这种情况下,完全阻止了 sql 注入(一阶和二阶)。


*事实证明,MySql/PHP在涉及宽字符时(很久很久以前)在处理参数方面是愚蠢的,而且在另一个高投票率的答案中概述了一个罕见的情况,可以允许注入通过参数化查询。

评论

6赞 Mark Biek 9/25/2008
这很有趣。我不知道一阶与二阶。您能详细说明一下二阶的工作原理吗?
197赞 cjm 9/26/2008
如果所有查询都已参数化,则还可以防止二阶注入。一阶注入是忘记了用户数据是不可信的。二阶注入是忘记了数据库数据是不可信的(因为它最初来自用户)。
6赞 Mark Biek 9/26/2008
谢谢cjm。我还发现这篇文章有助于解释二阶注入: codeproject.com/KB/database/SqlInjectionAttacks.aspx
50赞 troelskn 6/28/2011
啊,是的。但是三阶注入呢?必须意识到这些。
83赞 MikeMurko 11/3/2012
@troelskn,这必须是开发人员是不可信数据的来源
29赞 troelskn 9/25/2008 #3

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

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

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

评论

11赞 cHao 7/18/2012
“从不”夸大其词,以至于具有误导性。如果你错误地使用了准备好的语句,那并不比根本不使用它们好多少。(当然,将用户输入注入其中的“准备好的语句”违背了目的......但我实际上已经看到它完成了。预准备语句不能将标识符(表名等)作为参数进行处理。除此之外,一些 PDO 驱动程序会模拟准备好的语句,并且它们有错误地这样做的空间(例如,通过半途而废地解析 SQL)。简短版本:永远不要认为它那么容易。
45赞 Tower 4/21/2010 #4

不,并非总是如此。

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

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

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

评论

12赞 Rob Forrest 9/25/2012
我在这里是否遗漏了一些东西,但准备好的语句的重点不是避免将 sql 视为字符串吗?不会像 $dbh->prepare('SELECT * FROM :tableToUse where username = :username');解决你的问题?
4赞 Tower 9/27/2012
@RobForrest是的,你错过了:)。绑定的数据仅适用于 DDL(数据定义语言)。你需要这些引号和适当的转义。为查询的其他部分放置引号很可能会破坏它。例如,可能是错误的,因为它应该是错误的,或者没有任何背刺。然后,诸如用户来自哪里之类的事情就不能简单地逃脱了。因此,实际场景是相当无限的。SELECT * FROM 'table'SELECT * FROM `table`ORDER BY DESCDESC
11赞 Félix Adriyel Gagnon-Grenier 4/30/2014
我想知道 6 个人怎么会对一条评论投赞成票,建议对准备好的声明进行明显错误的使用。如果他们尝试过一次,他们就会立即发现使用命名参数代替表名是行不通的。
0赞 RN Kushwaha 9/13/2014
如果你想学习PDO,这里有一个很棒的教程。a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
11赞 ZiggyTheHamster 9/27/2014
切勿使用查询字符串/POST 正文来选择要使用的表。如果没有模型,至少使用 a 来派生表名。switch
33赞 PeeHaa 8/31/2012 #5

不,这还不够(在某些特定情况下)!默认情况下,PDO在使用MySQL作为数据库驱动程序时使用模拟的预准备语句。使用 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 注入?

请注意,这只能保护您免受 SQL 注入,但您的应用程序仍可能容易受到其他类型的攻击。例如,您可以通过再次使用正确的编码和引用样式来防止 XSS。htmlspecialchars()

916赞 ircmaxell 8/31/2012 #6

简短的回答是肯定的,如果使用得当,PDO 准备工作足够安全。


我正在调整这个答案来谈论 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格式进行编码,即 具有一些最终字节为 ASCII 的字符,即 .事实证明,MySQL 5.6 默认支持 5 种这样的编码:、、和 。我们将在这里选择。'0x27\0x5cbig5cp932gb2312gbksjisgbk

    现在,注意这里的用法非常重要。这将在 SERVER 上设置字符集。还有另一种方法可以做到这一点,但我们很快就会到达那里。SET NAMES

  2. 有效载荷

    我们将用于此注入的有效载荷从 字节序列 .在 中,这是一个无效的多字节字符;中,它是字符串。请注意,in 本身就是一个文字字符。0xbf27gbklatin1¿'latin1gbk0x27'

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

  3. $stmt->执行()

    这里要注意的重要一点是,默认情况下,PDO 执行真正的预准备语句。它模拟它们(对于MySQL)。因此,PDO 在内部生成查询字符串,对每个绑定字符串值调用(MySQL C API 函数)。mysql_real_escape_string()

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

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

    縗' OR 1=1 /*

    这正是攻击所需要的。

  4. 查询

    这部分只是一个形式,但下面是呈现的查询:

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

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

简单的解决方法

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

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

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

正确的修复方法

这里的问题是我们使用了 C API 而不是 .否则,攻击不会成功。但最糟糕的是,PDO 直到 5.3.6 才公开 C API,因此在以前的版本中,它无法阻止每个可能的命令的这种攻击! 它现在公开为 DSN 参数,应该使用它而不是 ...SET NAMESmysql_set_charset()mysql_set_charset()SET NAMES

前提是我们自 2006 年以来一直使用 MySQL 版本。如果您使用的是较早的MySQL版本,那么错误意味着无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节以进行转义,即使客户端已正确通知连接编码,此攻击仍会成功。该错误已在 MySQL 4.1.20、5.0.22 和 5.1.11 中修复。mysql_real_escape_string()

救赎的恩典

正如我们在一开始所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4 不容易受到攻击,但可以支持所有 Unicode 字符:因此您可以选择使用它——但它仅在 MySQL 5.5.3 之后才可用。另一种选择是 utf8,它也不容易受到攻击,并且可以支持整个 Unicode 基本多语言平面

或者,您可以启用 NO_BACKSLASH_ESCAPES SQL 模式,该模式(除其他外)会更改 的操作。启用此模式后,将替换为 而不是 ,因此转义过程无法在任何以前不存在的易受攻击的编码中创建有效字符(即 是静止的等)—因此,服务器仍将拒绝该字符串,因为该字符串无效。但是,请参阅@eggyal的答案,了解使用此 SQL 模式(尽管不是 PDO)可能产生的其他漏洞。mysql_real_escape_string()0x270x27270x5c270xbf270xbf27

安全示例

以下示例是安全的:

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 ≥中)

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

  • 启用 SQL 模式NO_BACKSLASH_ESCAPES

你是100%安全的。

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

补遗

我一直在慢慢地开发一个补丁,将默认值更改为不模拟,为PHP的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是,模拟的准备只会在执行时抛出语法错误,而真正的准备会在准备时抛出错误。因此,这可能会导致问题(这也是测试无聊的部分原因)。

评论

0赞 nico gawenda 4/17/2013
我认为“The Saving Grace”是MySQL 5.0.22修复程序:dev.mysql.com/doc/relnotes/mysql/5.0/en/news-5-0-22.html
1赞 ircmaxell 4/17/2013
@nicogawenda:这是一个不同的错误。在 5.0.22 之前,无法正确处理连接正确设置为 BIG5/GBK 的情况。因此,实际上即使调用 mysql < 5.0.22 也容易受到此错误的影响!所以不,这篇文章仍然适用于 5.0.22(因为 mysql_real_escape_string 只对来自 的调用进行了字符集,这就是这篇文章所说的绕过)......mysql_real_escape_stringmysql_set_charset()mysql_set_charset()
4赞 lepix 1/4/2016
请注意,还可能引入新的漏洞:stackoverflow.com/a/23277864/1014813NO_BACKSLASH_ESCAPES
1赞 slevin 1/25/2018
@ircmaxell 参加聚会有点晚了,但我不是专家,我真的在努力学习新东西,我无法理解如何以及为什么是成功的攻击。对我来说,这似乎是在寻找不存在的东西。评论是攻击吗?它让每个人都悬而未决?也许你可以为像我这样的菜鸟添加几行解释这部分。谢谢你的惊人回答。SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
3赞 ircmaxell 1/26/2018
@slevin,“OR 1=1”是你想要的任何内容的占位符。是的,它正在名称中搜索一个值,但假设“OR 1=1”部分是“UNION SELECT * FROM users”。您现在控制了查询,因此可以滥用它...
-1赞 snipershady 3/5/2018 #7

Eaven 如果你打算使用 html 或 js 检查来防止 sql 注入前端,你必须考虑前端检查是“可绕过的”。

您可以使用前端开发工具(现在内置于 firefox 或 chrome)禁用 js 或编辑模式。

因此,为了防止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 注入“;它不会阻止意外值,只会阻止意外的串联

评论

8赞 Your Common Sense 3/5/2018
您将 SQL 注入与其他东西混淆,这使得您的答案完全无关紧要