PDO 预准备语句是否足以防止 SQL 注入?攻击简单修复正确的修复拯救的恩典安全示例结束语补遗

2022-08-30 05:47:53

假设我有这样的代码:

$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

简短的答案是否定的,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中一样编码,即 有一些字符,其最终字节是ASCII,即.事实证明,默认情况下,MySQL 5.6中支持5种这样的编码:,,,和。我们将在此处选择。'0x27\0x5cbig5cp932gb2312gbksjisgbk

    现在,注意此处的用法非常重要。这将设置服务器上字符集。还有另一种方法可以做到这一点,但我们很快就会到达那里。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 而不是 。如果我们这样做了,只要我们从2006年开始使用MySQL版本,我们就会很好。mysql_set_charset()SET NAMES

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

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

拯救的恩典

正如我们在开始时所说,要使此攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。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的准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是,模拟的准备只会在执行时引发语法错误,但真正的准备会在准备时引发错误。因此,这可能会导致问题(并且是测试无聊的部分原因)。


答案 2

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

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

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

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

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

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


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


推荐