PreparedStatement防SQL注入的关键在于数据库服务端固化语法结构并隔离占位符为纯数据槽位,而非字符串拼接;其安全生效需同时满足PDO::ATTR_EMULATE_PREPARES=false、所有外部输入进占位符、绑定时显式指定类型。

预编译语句(PreparedStatement)能防 SQL 注入,不是因为它“更安全地拼字符串”,而是数据库服务端在收到 SELECT * FROM users WHERE id = ? 这类语句时,**立刻固化语法结构、锁定 ? 为纯数据槽位**——后续传入的任何内容,哪怕包含 '; DROP TABLE users; --,也只被当作字节流绑定,不参与 SQL 解析。
预编译的物理隔离发生在数据库服务端
客户端调用 prepareStatement() 不等于安全;关键看数据库是否真执行了服务端预编译。MySQL 在收到带 ? 的语句后会:
- 词法分析:识别出
WHERE、=、占位符位置,但不解析 ? 后面是什么 - 生成不可变语法树:
id = ?中的 ? 被标记为“整型/字符串槽位”,后续只接受二进制参数包 - 拒绝重解析:即使你传
setString(1, "1 OR 1=1"),数据库也只取第一个非空数字部分(即1),不会拼进 WHERE 条件
这个过程与字符集、引号转义、mysql_real_escape_string() 完全无关——它发生在协议层,绕过所有应用层字符串操作。
PDO::prepare() 安全的三个硬性条件缺一不可
PHP 的 PDO::prepare() 常被误认为“只要用了就安全”。实际上必须同时满足:
-
PDO::ATTR_EMULATE_PREPARES => false:否则 PDO 自己模拟预编译,退化为字符串拼接,宽字节场景下可被%df%27绕过 - 所有外部输入必须进占位符:写成
"WHERE name = '{$_POST['name']}'",prepare()就是摆设 - 绑定时显式指定类型:
bindValue(':id', $_GET['id'], PDO::PARAM_INT),而非依赖自动推断;用PDO::PARAM_STR传数字可能截断为0或空字符串且无警告
占位符根本不管不了表名、ORDER BY、LIMIT 这些地方
? 和 :name 只能在 VALUES、WHERE、SET 右侧出现。以下位置预编译完全无效,必须靠白名单或强校验:
-
ORDER BY ?→ 语法错误,必须写成ORDER BY+in_array($_GET['sort'], ['created_at', 'status'], true) ? $_GET['sort'] : 'id' -
LIMIT ?, ?→ 第二个 ? 是偏移量,必须intval($_GET['offset'])后拼接,且上限硬编码限制(如max(0, min(1000, $offset))) - 动态表名:
"SELECT * FROM " . $_GET['table']→ 即使后面跟prepare()也毫无意义,必须映射到预设枚举或拒绝用户控制
真正危险的不是不会写 setString(),而是以为写了 prepare() 就高枕无忧,结果把 $_GET['sort'] 直接塞进 ORDER BY 子句里——这种漏洞和预编译机制完全无关,只能靠逻辑层严格过滤。

















