引言:PDO 的神话与现实
\n\n如果你编写 PHP 代码,并且曾经在谷歌上搜索过“如何防范 SQL 注入”,你很可能会听到这样一句格言:“使用 PDO 和预处理语句——这样你就安全了”。这是黄金标准,是所有 PHP 安全教程的基础。但这真的对吗?仅仅编写 $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?') 然后就不再担心问题,真的够了吗?
“PDO 预处理语句是否足以防止 SQL 注入?”这个问题在 Stack Overflow 上获得了数千次浏览,这是有原因的。事实证明,答案并非那么简单。在本文中,我们将深入 PDO 的内部机制,剖析预处理语句的实际工作原理,并找出在哪些情况下,即使是 PDO 也可能让你失望。
\n\n这不是枯燥的理论。我们将通过实际示例、查询模拟、编码细节来探讨,并提供具体的行动项,让你的代码真正安全。
\n\n预处理语句是如何工作的:魔法还是数学?
\n\n首先,让我们回顾一下什么是 SQL 注入。当攻击者将数据传入查询,而这些数据被解释为 SQL 代码的一部分时,就发生了 SQL 注入。经典例子:' OR 1=1 --。
预处理语句从根本上解决了这个问题。它将查询逻辑与数据分离开来。看起来像这样:
\n\n<?php\n$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');\n$stmt->execute([':email' => $_GET['email'], ':status' => 'active']);\n?>\n\n\n在底层,发生了以下过程:
\n\n- \n
- 准备阶段:驱动程序向数据库服务器发送一个查询“骨架”:
SELECT * FROM users WHERE email = ? AND status = ?。服务器解析这个骨架,对其进行优化,并记住执行计划。数据的占位符保持为空。 \n - 执行阶段:现在你只发送实际的值。服务器将这些值插入到已准备好的计划中,但它是通过一种特殊方式进行的——它知道这些是数据,而不是代码。因此,任何试图“闭合引号”或添加 SQL 命令的操作,都只会成为字符串字面量的一部分。 \n
听起来很完美。但这里有一个陷阱。
\n\n陷阱:模拟的预处理语句
\n\n那个 Stack Overflow 问题揭示的主要意外是:PDO 默认情况下并不总是使用“真正的”预处理语句。
\n\n问题在于,并非所有数据库都在协议层面支持此功能。而某些驱动程序(例如,用于 SQLite 或旧版 MySQL 的驱动程序)通过模拟来工作。这意味着什么?
\n\n当启用了模拟时,PDO 会在客户端(PHP 端)将值替换到 SQL 查询中,然后将完整的查询发送到服务器。它通过使用 PDO::quote() 转义特殊字符来实现这一点。
而问题就出在这里。模拟本质上就是同样的字符串替换,只不过由库来执行。如果你错误地设置了连接编码,这个机制就可能被破坏。
\n\n示例:通过不同编码进行的攻击
\n\n一个使用错误编码的经典攻击方式如下。假设你使用 GBK 编码(一种中文编码)建立 MySQL 连接,并且查询是模拟的。在 GBK 编码中,\\(反斜杠)字符可以是多字节字符的一部分。
攻击者发送字符串:%BF%27。在 GBK 编码中,序列 %BF%5C 构成一个字符,而 %27 只是一个引号。模拟器看到引号并对其进行转义,将 %27 转换为 %5C%27(即 \\')。但是,由于