where()中字符串拼接会引发SQL注入,因框架将其视为原生SQL不转义;安全做法是用数组语法、闭包param()或bind()绑定参数,禁用exp/raw等绕过防护的表达式,并启用strict()强化校验。

为什么 where() 里拼接字符串会直接触发 SQL 注入
ThinkPHP 查询构造器本身不自动过滤字符串拼接,一旦你把用户输入直接塞进 where() 的第一个参数(尤其是字符串格式),框架就当它是原生 SQL 片段来处理,不会做任何转义或参数绑定。
常见错误现象:where("username = '" . input('username') . "'") —— 这种写法和裸写 PDO::query 几乎没区别,' OR 1=1 -- 一输进去,全表数据就出来了。
- 使用场景:搜索框、后台筛选条件、URL 参数驱动的查询
- 真正安全的做法是始终用数组语法或闭包 +
param(),让框架走预处理流程 - 如果必须用字符串条件,只能配合
bind()显式绑定,且变量必须单独传入,不能在字符串里拼
示例(危险):where("status = " . input('status'))
示例(安全):where(['status' => input('status')]) 或 where('status = ?', [input('status')])
如何正确使用 where() 数组语法避免绕过校验
数组语法看似简单,但有几个关键点决定它是否真能防注入:键名必须是字段名,值必须是标量或支持自动转换的类型(如数组表示 IN 查询),不能是表达式字符串。
常见错误现象:where(['username' => "admin' OR 1=1 -- "]) 看似用了数组,但值本身含恶意 SQL,而 ThinkPHP 默认对字符串值不做二次解析 —— 这其实是安全的;但如果你写成 where(['username' => ['exp', "='admin' OR 1=1 -- "]]),就等于主动打开后门。
立即学习“PHP免费学习笔记(深入)”;
-
['field' => 'value']:安全,走参数绑定 -
['field' => ['like', '%abc%']]:安全,框架内部处理 -
['field' => ['exp', 'UPPER(username) = "ADMIN"']]:危险,exp表示“按原样执行”,跳过所有防护 - 所有带
exp、raw、think\helper\Str::random()类动态生成 SQL 的地方,都要人工审计
select() 前忘记调用 strict() 会放大风险
ThinkPHP 默认开启 sql_explain 和宽松模式,某些边界情况(比如空数组条件、嵌套查询中混用字符串与数组)可能导致条件被忽略或降级为恒真,进而绕过本应生效的 where 限制。
使用场景:多条件组合查询、权限控制中间件、分页前的 count 查询
- 显式调用
strict(true)可让框架拒绝执行非法或模糊条件,比如where([])直接抛异常,而不是返回全表 - 搭配
failException(true)使用,能让问题暴露在开发/测试阶段,而不是上线后被扫出漏洞 - 性能影响几乎为零,但兼容性需注意:老项目若依赖空条件返回全部数据,加
strict()后行为会变
示例:Db::name('user')->strict(true)->where($cond)->select()
自定义查询方法里最容易漏掉的两个防护点
封装通用查询逻辑时,开发者常只关注业务逻辑,却忽略底层 SQL 构造是否延续了安全链路。最典型的是手动拼接 ORDER BY 字段和动态 FIELD 排序。
常见错误现象:order(input('sort') . ' ' . input('order')) —— sort 是字段名,order 是 ASC/DESC,两者都来自用户输入,且无法用参数绑定
- 字段白名单校验不可省:
in_array(input('sort'), ['id', 'create_time', 'status'], true) -
ORDER BY方向必须硬编码或映射:input('order') === 'desc' ? 'DESC' : 'ASC',绝不能直插 - 所有通过
__call()动态代理的查询方法(如whereLikeName()),要检查其内部是否仍走数组语法,而非字符串拼接
复杂点在于:SQL 注入不一定发生在 WHERE,ORDER BY、GROUP BY、LIMIT 同样可被利用,而 ThinkPHP 对这些子句的防护远不如 where() 严密。



















