ThinkPHP 的 SQL 防御仅在使用数组或闭包传参时触发预处理,字符串拼接直接绕过;动态字段名须白名单校验,PDO 模拟预处理必须显式关闭。

where 传数组或闭包才触发自动防御,字符串拼接直接绕过——这不是框架没防,是你没用对方式。
为什么 where("id = " . input('id')) 会中招
ThinkPHP 的查询构造器只在明确识别到「安全参数结构」时才启用 PDO 预处理。传字符串进去,框架就当你是写死的 SQL 片段,不做任何绑定,直接拼接执行。
常见错误现象:input('id') 是 '1 OR 1=1 --',最终生成 WHERE id = '1 OR 1=1 --',PDO 根本不介入。
- ✅ 安全写法:
where('id', input('id'))或where(['id' => input('id')])→ 自动转成WHERE id = ?并绑定值 - ✅ 复杂条件用闭包:
where(function ($q) { $q->where('name', input('name'))->where('status', 1); }) - ❌ 危险写法:
where("id = " . input('id'))、where("name LIKE '%" . input('kw') . "%') - ⚠️ 特别注意:
like不能自己拼%,必须用数组语法:['like', '%' . input('kw') . '%']
Db::query() 必须手动加 ? 占位符
Db::query() 和 Db::execute() 是裸通道,框架不解析 SQL 字符串内容,只看你有没有传参数数组 + 占位符。命名占位符(如 :name)在 ThinkPHP 6 默认配置下不生效,除非你额外开启 params_bind => true,但会引入兼容性风险。
立即学习“PHP免费学习笔记(深入)”;
- ✅ 推荐写法:
Db::query("SELECT * FROM user WHERE status = ? AND type = ?", [$status, $type]) - ✅ IN 查询要动态生成占位符:
$placeholders = implode(',', array_fill(0, count($ids), '?'));,再调用Db::query("WHERE id IN ($placeholders)", $ids) - ❌ 错误写法:
Db::query("WHERE id IN (?)", [[1,2,3]])→ 实际变成IN ('1,2,3'),语法报错 - ❌ 绝对禁止:
Db::query("SELECT * FROM {$table} WHERE id = " . $id)→ 表名和值双注入点
动态字段名(order / group / having)必须硬编码白名单
PDO 不允许把字段名、排序方向、聚合函数名当参数绑定——这些属于 SQL 结构,不是数据。闭包也救不了,order(input('sort')) 在闭包里照样危险。
- ✅ 字段校验:
$sort = in_array(input('sort'), ['id', 'create_time', 'status']) ? input('sort') : 'id'; - ✅ 方向校验:
$order = input('order') === 'desc' ? 'DESC' : 'ASC'; - ✅ 拼接安全:
order($sort . ' ' . $order) - ❌ 危险拼接:
order(input('sort') . ' ' . input('order'))→ 攻击者可填入id; SELECT password FROM admin - ⚠️
Db::raw()和exp()完全绕过绑定,除非你先用filter_var()或正则清洗输入,否则等于交出数据库权限
底层驱动必须关掉 PDO::ATTR_EMULATE_PREPARES
即使你全用数组写法,只要 PDO 驱动层开了模拟预处理(PDO::ATTR_EMULATE_PREPARES = true),所有 ? 都会退化为字符串拼接。这在 Docker 环境或 MySQL 5.6 及以下版本中极常见,不是框架 bug,是配置失效。
- ✅ 显式关闭:在
config/database.php或.env的数据库params中加PDO::ATTR_EMULATE_PREPARES => false - ✅ 验证是否生效:查
runtime/log/sql.log,看到Binding: [123]才算真绑定;只有SQL: WHERE id = ?是假防护 - ✅ 部署模式设为单机:
deploy = 0,集群下连接复用可能跳过该设置 - ⚠️
input()返回值默认无过滤,不强制类型转换时,'id' => '1 OR 1=1'仍会被原样绑定——模型层需配$type或用input('id', 0, 'intval')
最易被忽略的是:动态字段名无法参数化,白名单必须硬编码;而 input() 的返回值默认是字符串,不做类型约束时,数字型字段照样能塞进恶意表达式。



















