小编典典

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

all

假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO 文档说:

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

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

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


阅读 121

收藏
2022-03-03

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

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

  1. 有效载荷

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

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

  1. $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 /*

这正是攻击所需要的。

  1. 查询

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

    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等)——因此服务器仍将拒绝该字符串为无效的。但是,了解使用此 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 的 Modern Versions (late 5.1, all 5.5, 5.6, etc) AND PDO’s DSN charset parameter (in PHP ≥ 5.3.6)

要么

  • 不要使用易受攻击的字符集进行连接编码(你只使用utf8///等latin1ascii

要么

  • 启用NO_BACKSLASH_ESCAPESSQL 模式

你是 100% 安全的。

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

附录

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

2022-03-03