必须关闭PDO::ATTR_EMULATE_PREPARES,否则ThinkPHP所有绑定(包括Db::query和where)均退化为PHP层字符串拼接,宽字节等场景下SQL注入可穿透;运行时需用getAttribute()验证返回false才算生效。

ThinkPHP里PDO::ATTR_EMULATE_PREPARES设为true会直接绕过SQL注入防护
只要这个配置是true,哪怕你写了where(['id' => $id])或Db::query("SELECT * FROM user WHERE id = ?", [$id]),底层仍是字符串拼接——PDO自己把?替换成值再发给MySQL,没走服务端预编译。攻击者在宽字节环境(如GBK)、MySQL低版本或Docker默认配置下,用%df' OR 1=1 --这类payload就能穿透。
验证是否生效最简单的方法:执行$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES),返回false才算真正关闭。别只看配置文件写了没,得运行时确认。
- ThinkPHP 6.x 默认不显式设置该参数,依赖PDO扩展自身行为,而PHP 8.2+仍默认开启模拟预处理
- 在
config/database.php的params里加PDO::ATTR_EMULATE_PREPARES => false,不是可选项,是硬性要求 - 如果项目用了连接池或长连接复用(如Swoole协程模式),必须确保每次新连接都重置该属性,否则可能被复用旧连接导致失效
为什么ThinkPHP原生SQL绑定(Db::query)也受它影响
Db::query()和Db::execute()看似“显式绑定”,但底层调用的仍是PDO的prepare() + execute()。一旦PDO::ATTR_EMULATE_PREPARES为true,Db::query("WHERE id = ?", [$id])实际发出的SQL就是WHERE id = '1'这种拼接结果,日志里看到Binding: [1]只是假象,sql.log里查不到真实绑定记录。
- 开启
'sql_explain' => true后,检查日志中是否出现Binding:行;只有Binding:与SQL:同时存在且值一致,才说明绑定真实生效 - 用命名占位符(如
:name)时,键名必须严格匹配,':name '(带空格)会导致绑定失败且无报错,除非PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION已启用 -
IN语句不能直接传数组:Db::query("id IN (?)", [[1,2,3]])无效,必须手动构造?, ?, ?占位符再绑定
ThinkPHP模型层自动绑定不等于绝对安全
数组格式where(['name' => $input])和闭包where(function($q) use($name){$q->where('name', $name);})确实触发框架自动绑定,但前提是整个链路没被中途打断。一旦混入raw()、exp()、字符串拼接字段名或force(true),绑定就失效。
立即学习“PHP免费学习笔记(深入)”;
- 禁止在
where里写['exp', "status = 1 AND name LIKE '%{$kw}%'"],这等于主动打开注入口 - 动态排序字段(如
order(input('sort')))必须白名单校验:in_array(input('sort'), ['id', 'create_time']) ?: 'id',不能靠addslashes()或正则过滤 - 模型的
field()、table()、alias()等方法接收的都是标识符,不是值,它们从不参与绑定,必须提前约束范围
部署环境(尤其是Docker)最容易忽略PDO模拟预处理的实际状态
Docker镜像里的PHP往往没改PDO默认行为,MySQL服务端也常是5.7或更低版本,默认允许模拟预处理。你以为配置写了就完事,其实容器启动后PDO::ATTR_EMULATE_PREPARES还是true,而且连phpinfo()都看不到这个值,只能靠代码验证。
- 在
public/index.php入口处加一行:var_dump((new \think\facade\Db())->getPdo()->getAttribute(PDO::ATTR_EMULATE_PREPARES));die;,上线前必须跑一次 - 不要依赖
php.ini全局设置pdo.default_attribute[1002] = 0,ThinkPHP初始化数据库连接时会覆盖它 - 集群或微服务场景下,每个服务实例都要单独验证,不能只测一台
真实防御的关键不在“写了绑定”,而在“绑定有没有被底层悄悄降级”。PDO模拟预处理就是那个最隐蔽的降级开关,关不掉,其他所有防护动作都可能形同虚设。



















