必须设为false,因默认true时PHP在客户端拼接SQL,无法实现SQL与参数物理隔离;仅设false才能启用MySQL服务端原生预处理,确保参数不被解析为语法结构。

用 PDO 预处理语句防止 SQL 注入,不是“加个 prepare() 就安全了”,关键在三点:禁用模拟预处理、占位符只用于值、表名/字段名必须白名单校验。
为什么 PDO::ATTR_EMULATE_PREPARES => false 必须设
MySQL 驱动默认开启模拟预处理(PDO::ATTR_EMULATE_PREPARES => true),即 PHP 在客户端把参数拼进 SQL 字符串再发给数据库——这和字符串拼接没本质区别,只是多了层转义,仍可能被绕过(比如宽字节注入、多字节编码边界场景)。
真实预处理要求数据库服务端编译 SQL 模板,参数以独立协议包传输,数据库天然拒绝将参数解析为语法结构。
- 设置方式必须在
new PDO()后立即调用:$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false) - 仅对 MySQL 有效;PostgreSQL、SQLite 默认就是真实预处理,设了也无副作用
- 若设为
false后报错SQLSTATE[HY000]: General error: 2025,说明 MySQL 版本太低(
prepare() 的占位符只能填值,不能填表名或字段名
这是最多人踩坑的地方。下面这段代码看似参数化,实则完全不防注入:
立即学习“PHP免费学习笔记(深入)”;
$table = $_GET['tab'];
$stmt = $pdo->prepare("SELECT * FROM {$table} WHERE id = ?");
$stmt->execute([$id]);
因为 {$table} 是 PHP 字符串拼接,不是占位符,prepare() 根本不处理它。攻击者传 tab=users; DROP TABLE users-- 就能直接执行。
正确做法只有两个:
- 表名/字段名走硬编码或严格白名单:
$allowed_tables = ['users', 'posts', 'comments']; if (!in_array($table, $allowed_tables)) die('Invalid table'); - 用
sprintf()或str_replace()做字符过滤(仅限 ASCII 字母+下划线),但不如白名单可靠
命名占位符 :name 比问号 ? 更适合维护
当 SQL 中有多个相同值(比如 WHERE a = ? AND b = ? AND c > ?),用问号容易绑错顺序;命名占位符可读性强,且支持重复使用同一参数。
示例:
$sql = "SELECT * FROM users
WHERE status = :status
AND created_at BETWEEN :from AND :to
ORDER BY :sort_field DESC
LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':status', 'active', PDO::PARAM_STR);
$stmt->bindValue(':from', '2026-01-01', PDO::PARAM_STR);
$stmt->bindValue(':to', '2026-12-31', PDO::PARAM_STR);
$stmt->bindValue(':sort_field', 'created_at'); // 注意:这里不能参数化!见上一节
$stmt->bindValue(':limit', 10, PDO::PARAM_INT);
$stmt->execute();
注意::sort_field 这种字段名占位符是非法的,PDO 会直接抛出 SQLSTATE[HY093] 错误——占位符只接受值类型(PDO::PARAM_STR、PDO::PARAM_INT 等),不接受标识符。
bindValue() 和 bindParam() 别混用
两者核心区别在于绑定时机和变量生命周期:
-
bindValue(':name', $value):立刻把$value的当前值拷贝进语句,后续改$value不影响已绑定内容 -
bindParam(':name', $value):绑定的是变量引用,执行时才取$value当前值;适合循环中复用同一语句,但要注意变量作用域(比如foreach中未用&$v引用会导致最后全绑定成最后一个值) - 对简单场景,直接传数组给
execute()最省事:$stmt->execute([':name' => $name, ':email' => $email]),等价于多次bindValue()
最易忽略的点:字符集必须在 DSN 中显式声明,比如 mysql:host=localhost;dbname=testdb;charset=utf8mb4。漏掉 charset 会导致 prepare() 阶段就发生宽字节截断,让某些注入绕过真实预处理保护。



















