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

新手上路,请多包涵

假设我有这样的代码:

 $dbh = new PDO("blahblah");

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

PDO 文档说:

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

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

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

原文由 Mark Biek 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 276
2 个回答

简短的回答是 肯定 的,如果使用得当,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 即 0x27 并且 具有一些最终字节为 ASCII 的字符 \0x5c 。 As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5 , cp932 , gb2312 , gbk and sjis 。我们将在这里选择 gbk

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

  1. 有效载荷

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

We have chosen this payload because, if we called `addslashes()` on it, we'd insert an ASCII `\` ie `0x5c` , before the `'` 性格。所以我们最终会得到 `0xbf5c27` ,其中 `gbk` 是一个两个字符序列: `0xbf5c` 后跟 09278d6da8--- 09278d6da8 `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() 的调用会插入反斜杠,我们的“转义”内容中有一个自由悬挂的 ' 字符!事实上,如果我们查看 $var gbk 字符集中的 —,我们会看到:

   缞' 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 本身无法准备的语句:那些它可以在手册中 列出 的语句,但要注意选择适当的服务器版本)。

正确的修复

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

这是因为我们使用自 2006 年以来的 MySQL 版本。如果您使用的是较早的 MySQL 版本,那么 mysql_real_escape_string() 中的 错误 意味着无效的多字节字符(例如我们的有效负载中的那些)被视为单字节 _即使客户端已被正确告知连接编码_,也可以逃避目的,因此这种攻击仍然会成功。该错误已在 MySQL 4.1.20、5.0.225.1.11修复

拯救恩典

正如我们一开始所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 utf8mb4 _不易受攻击_,但可以支持 每个 Unicode 字符:因此您可以选择使用它,但它仅在 MySQL 5.5.3 之后才可用。另一种选择是 utf8 ,它也 _不易受到攻击,并且可以支持整个 Unicode [基本多语言平面](http://en.wikipedia.org/wiki/Plane(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 的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这可能会导致问题(并且是测试失败的部分原因)。

原文由 ircmaxell 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

原文由 PeeHaa 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题