ThinkPHP 的 where() 默认不防 SQL 注入,因字符串条件直接拼接SQL;仅数组、三元语法、闭包形式触发PDO预处理绑定,而动态字段、exp表达式、模拟预处理等场景会绕过绑定。

ThinkPHP 的 where() 为什么默认不防 SQL 注入?
因为 where() 接收字符串条件时,底层直接拼接进 SQL,不走 PDO 预处理——这是最常踩的坑。比如 where('id = ' . input('id')) 或 where("name like '%{$_GET['q']}%'"),变量一没过滤,SQL 注入就成立了。
真正触发 PDO 参数绑定的,是数组或闭包形式的查询条件,例如:where(['id' => $id])、where('status', 'eq', $status)、where(function ($query) { $query->where('id', input('id')); })。这些写法会让 ThinkPHP 自动转为占位符 + bindValue() 调用。
- 字符串条件(
where('id = '.$id))→ 拼接执行,无绑定 - 键值数组(
where(['id' => $id]))→ 绑定执行,安全 - 三元语法(
where('id', '>', 10))→ 绑定执行,安全 - 闭包查询(
where(function($q){...}))→ 内部仍需遵守绑定规则
手动绑定失效的三种典型场景
即使用了数组写法,也可能因类型推断失败或手动干预导致绑定跳过。常见于字段名动态拼接、表达式混用、或显式关闭预处理。
-
where("{$field} = ?", $value):问号占位符看似能绑,但 ThinkPHP 5.1+ 默认禁用该语法,会直接报错或回退为字符串拼接 -
where(['id' => ['exp', "1 or 1=1"]]):用exp表达式绕过绑定,exp含义就是“原样插入”,等同于拼接 -
Db::connect(['deploy' => 0])->table('user')->where(...):某些连接配置下 PDO 的PDO::ATTR_EMULATE_PREPARES被设为true,导致预处理被模拟(即实际仍是拼接),尤其在 MySQL 低版本或 Docker 环境中容易出现
如何验证当前查询是否真走了 PDO 绑定?
不能只看代码写法,得看最终执行的 PDO 行为。最可靠的方式是开启 ThinkPHP 的 SQL 日志并观察参数传递过程。
立即学习“PHP免费学习笔记(深入)”;
- 在配置中打开
'sql_explain' => true和'debug' => true,然后查日志里是否有类似Binding: [123]这样的绑定记录 - 用 Xdebug 断点跟到
think\db\Connection::execute(),看$pdoStatement->bindValue()是否被调用(而非execute($params)直接传数组) - 在 MySQL 服务端开启
general_log,对比日志中是出现WHERE id = ?还是WHERE id = 123—— 前者说明 PDO 层做了绑定,后者说明已被模拟或跳过
注意:日志里显示 SQL: SELECT * FROM user WHERE id = ? 并不等于真的绑定成功,只是 ThinkPHP 的日志美化输出;必须确认底层 PDO 调用行为。
自定义 SQL 中安全传参的唯一可靠方式
当必须写原生 SQL(比如复杂子查询、窗口函数、UNION),query() 或 execute() 的参数数组是唯一安全路径,其他方式全是高危区。
- ✅ 正确:
Db::query('SELECT * FROM user WHERE status = ? AND created_time > ?', [$status, $time]) - ❌ 危险:
Db::query("SELECT * FROM user WHERE status = {$status}") - ❌ 危险:
Db::query('SELECT * FROM user WHERE status = ' . $status) - ⚠️ 表面安全实则失效:
Db::query('SELECT * FROM user WHERE status = :status', [':status' => $status])—— ThinkPHP 不支持命名占位符,会忽略绑定,直接拼接
命名占位符(:name)在 ThinkPHP 中完全不识别,只认问号(?)顺序绑定。这点和 PDO 原生不同,也是很多人翻车的地方。











