pdo::attr_emulate_prepares必须设为false才能启用真正的服务端预编译防护,设为true会导致所有绑定操作退化为php层字符串拼接,宽字节等场景下易被sql注入穿透,需运行时用getattribute()验证是否生效。

ThinkPHP里PDO::ATTR_EMULATE_PREPARES设为true等于关掉SQL注入防护
只要 PDO::ATTR_EMULATE_PREPARES 是 true,ThinkPHP所有绑定操作——包括 where(['id' => $id])、Db::query("SELECT * FROM user WHERE id = ?", [$id])——都会退化成PHP层字符串拼接。PDO自己把 ? 替换成值再发给MySQL,没走服务端预编译。
宽字节环境(如GBK)、MySQL低版本或Docker默认配置下,攻击者用 %df' OR 1=1 -- 这类payload就能穿透防护。
- 不是“框架没做绑定”,而是绑定根本没生效,日志里看到的
Binding: [1]是假象 -
sql.log里查不到真实绑定记录,只看到SQL: SELECT * FROM user WHERE id = ? - ThinkPHP 6.x 默认不显式设置该参数,依赖PDO扩展自身行为;而PHP 8.2+仍默认开启模拟预处理
怎么确认PDO::ATTR_EMULATE_PREPARES已真正关闭
别只看配置文件写了没,必须运行时验证。最简单的方法是执行:
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)
返回 false 才算生效。返回 true 或报错说明没关掉。
- 在
config/database.php的params里加PDO::ATTR_EMULATE_PREPARES => false,不是可选项,是硬性要求 - 如果用了Swoole协程、连接池或长连接复用,每次新连接都得重置该属性,否则可能复用旧连接导致失效
- 部署模式为集群或
deploy != 0时,连接复用逻辑更复杂,需额外检查连接初始化流程
ThinkPHP原生SQL绑定(Db::query)也受它影响
Db::query() 和 Db::execute() 看似“显式绑定”,但底层调用的仍是PDO的 prepare() + execute()。一旦 PDO::ATTR_EMULATE_PREPARES 为 true,实际发出的SQL就是 WHERE id = '1' 这种拼接结果。
-
:name绑定时,键名必须严格匹配,':name '(带空格)会导致绑定失败且无报错,除非已启用PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION -
IN语句不能直接传数组:Db::query("id IN (?)", [[1,2,3]])无效,必须手动展开成?, ?, ?占位符再绑定 - 混入
raw()、exp()、字符串拼接字段名或force(true),整个链路的绑定就失效
动态字段和模型层白名单校验不能替代这个配置
入口层过滤、in_array(input('sort'), ['id', 'create_time']) 白名单、模型字段限制,这些都重要,但它们是第二道防线。如果 PDO::ATTR_EMULATE_PREPARES 没关,第一道防线已经崩了。
- 禁止在
where里写['exp', "status = 1 AND name LIKE '%{$kw}%'"],这等于主动打开注入口 - 模型的
field()、allowField()只防字段名注入,不防值注入;而PDO::ATTR_EMULATE_PREPARES => false是值注入的底层保障 - 安全基线不是靠“多加一层”,而是确保每层都不可绕过;这一项漏掉,其他防护全打折扣
真正生效的预处理必须由数据库服务器执行,不是PDO模拟出来的。很多项目线上跑着,getAttribute() 一查还是 true,这种状态下的“参数绑定”只是心理安慰。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











