PHP 8 彻底防御 SQL 注入需三要素:关闭模拟预处理(PDO::ATTR_EMULATE_PREPARES=false)、值位置用占位符+显式类型绑定、非值上下文(如表名、ORDER BY)严格白名单校验,缺一不可。

PHP 8 中彻底防御 SQL 注入,只靠 PDO::prepare() 不够——必须关掉模拟预处理、显式绑定类型、白名单校验非值上下文,三者缺一不可。
为什么 PDO::prepare() 单独调用不防注入
很多人写 $pdo->prepare("SELECT * FROM users WHERE name = '{$_POST['name']}'"),这根本没用:字符串已拼完,prepare() 只是对一条“静态 SQL”做编译,用户输入早被当代码执行了。真正起作用的是占位符 + 后续绑定的组合。
- 占位符只能用于 值的位置(WHERE 条件、INSERT 的 VALUES、UPDATE 的 SET 值),不能用于表名、字段名、ORDER BY、GROUP BY、LIMIT 数字等
-
prepare()返回的PDOStatement对象本身不执行任何防护,防护发生在bindValue()、bindParam()或execute()传参时 - 如果没绑定、或绑定前就拼接了变量,等于裸奔
PDO::ATTR_EMULATE_PREPARES => false 是硬性要求
PHP 默认开启模拟预处理(PDO::ATTR_EMULATE_PREPARES = true),此时 PDO 自己替换 ? 占位符再发整条 SQL 给 MySQL——和手动拼接无异,宽字节、编码错配时照样被绕过。
- 必须在构造
PDO实例时显式关闭:new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false]) - MySQL 5.1.17+ 均支持原生预处理,无需担心兼容性
- 可通过
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)检查是否生效,返回false才算成功
数字参数不加引号,但必须走占位符 + 类型绑定
像 id = $_GET['id'] 这种场景,即使输入是纯数字,也不能直接拼接——因为攻击者可传 id=1 OR 1=1,数据库会按布尔表达式解析,mysqli_real_escape_string() 对它完全无效。
立即学习“PHP免费学习笔记(深入)”;
- 正确做法:用
?占位符,再用bindValue(1, (int)$_GET['id'], PDO::PARAM_INT) - 不要依赖
(int)强转后拼进 SQL 字符串,那只是“看起来像数字”,仍属拼接 -
PDO::PARAM_INT能防止浮点截断、科学计数法绕过等边缘 case;PDO::PARAM_STR确保字符串被自动加引号并转义
ORDER BY / 表名 / 字段名必须白名单校验,不能碰占位符
ORDER BY ? 是语法错误,MySQL 不允许参数化排序字段。同理,SELECT ? FROM users 或 FROM ? 全部非法。这些地方必须靠白名单兜底。
- 定义允许字段:
$allowed_fields = ['id', 'name', 'email', 'created_at']; - 提取并校验:
$sort = $_GET['sort'] ?? 'id'; if (!in_array($sort, $allowed_fields)) { die('Invalid sort field'); } - 排序方向同样处理:
$order = strtoupper($_GET['order'] ?? 'ASC'); if (!in_array($order, ['ASC', 'DESC'])) { die('Invalid order direction'); } - 拼接时不用引号:
"ORDER BY {$sort} {$order}",然后用$pdo->query($sql)(注意不是prepare())
最容易被忽略的是:白名单校验必须在 prepare 之前完成,且失败时必须中止执行——哪怕只漏一个 die 或 throw,整个防御链就断了。



















