ThinkPHP 防止 SQL 注入的核心是强制使用参数绑定,where 条件用数组或闭包,原生 SQL 必须显式绑定占位符,字段名类参数走白名单校验。
先说结论:ThinkPHP 框架本身不自动拦截 SQL 注入,安全取决于查询写法。使用查询构造器或 ORM 模型的数组条件基本安全,一旦拼接字符串或绕过绑定机制就会失守。
- 先判断:检查项目中 where()、query()、execute()、order() 等方法的调用方式
- 优先做:将所有字符串条件改为数组或闭包,原生 SQL 加上占位符和绑定参数
- 再验证:开启 SQL 日志确认 PDO 参数绑定真实生效,而非仅显示占位符
核心原理
ThinkPHP 的查询构造器只有在接收数组或闭包时才会触发 PDO 参数绑定流程。当你写 where(['id' => $id]) 时,框架会把 $id 作为参数单独发送给数据库,SQL 结构预先编译,数据后填充,这样即使 $id 包含恶意代码也会被当作普通字符串处理。
但如果你写 where("id = " . $id),PHP 会先在服务端拼接字符串,再把完整 SQL 发给数据库。此时 $id 里的任何内容都会直接进入 SQL 语句,攻击者传入 1 OR 1=1 就能绕过条件限制。
Db::query() 和 Db::execute() 是纯透传接口,不做任何过滤或绑定,它把你的字符串原样交给 PDO。不手动绑定占位符,等于裸奔。
还有一个隐蔽风险:PDO 的模拟预处理模式。当 PDO::ATTR_EMULATE_PREPARES 设为 true 时,预处理会退化为字符串拼接,尤其在 MySQL 低版本或某些 Docker 环境中容易触发,导致绑定机制失效。
分步处理
第一步:排查 where 条件写法
在项目中搜索 where( 关键字,检查以下条件类型:
危险写法示例:
where("id=".input('id')." AND status=1")——变量直接拼接,无绑定
where("name LIKE '%{input('kw')}%'")——双引号内变量会被 PHP 解析
安全写法改造:
where(['id' => input('id'), 'status' => 1])——数组形式,自动绑定
where('name', 'like', '%' . input('kw') . '%')——链式调用,内部处理绑定
复杂条件用闭包:
where(function ($q) { $q->where('name', input('name')); $q->where('status', 1); })
第二步:处理原生 SQL 绑定
位置占位符配索引数组:
Db::query("SELECT * FROM user WHERE id = ? AND status = ?", [$id, $status])
命名占位符配关联数组:
Db::query("SELECT * FROM user WHERE id = :id AND status = :status", [':id' => $id, ':status' => $status])
不要混用两种占位符,否则部分参数会静默失效。
第三步:IN 查询特殊处理
不能直接把数组传给绑定参数,ThinkPHP 会把它当单个字符串处理,生成 WHERE id IN ('1,2,3') 的错误 SQL。
正确做法是动态生成占位符:
$ids = [1, 2, 3];
$placeholders = str_repeat('?,', count($ids) - 1) . '?';
Db::query("SELECT * FROM user WHERE id IN ($placeholders)", $ids);
更省心的方式是使用链式调用:
Db::name('user')->where('id', 'in', $ids)->select()——内部已处理好 IN 绑定逻辑
空数组需提前判断,否则 IN () 会触发 SQL 语法错误。
第四步:关闭 PDO 模拟预处理
在数据库配置文件中设置。注意路径差异:
- ThinkPHP 5:通常位于
config/database.php - ThinkPHP 6:通常位于
config/database.php或配合.env配置,建议在配置数组中统一设置
配置示例:
'params' => [
PDO::ATTR_EMULATE_PREPARES => false
]
这能确保预处理是真实的,而非 PHP 层面的字符串替换。
第五步:字段名白名单校验
order()、group()、having() 等方法接收的是字段名或关键字,不能当数据传给 PDO 绑定。必须用白名单校验:
$sort = in_array(input('sort'), ['id', 'create_time', 'status']) ? input('sort') : 'id';
order($sort . ' ' . (input('order') === 'desc' ? 'DESC' : 'ASC'));
禁用 raw()、exp() 等危险接口,如需动态字段排序,改用白名单校验。
怎么验证是否生效
1、开启应用调试模式,在配置文件中设置 app_debug => true
2、启用 SQL 日志记录,确保 log 配置中记录了 SQL 语句
3、检查运行时日志目录(通常为 runtime/log/),执行一次含用户输入的查询
4、确认日志中出现 Binding: [value] 记录,而非仅显示 SQL: WHERE id = ?
5、如果有 Binding 记录,说明 PDO 参数绑定真实生效;如果只有 SQL 占位符没有绑定值,可能是模拟预处理未关闭或绑定写法有误
6、可用测试输入验证:在查询参数中传入 1 OR 1=1,观察返回结果是否被限制在预期范围内
常见坑
1、以为加了单引号就安全——where("name = '" . input('name') . "'") 中 PHP 照样解析变量,攻击者输 admin' OR '1'='1 就能闭合引号注入
2、IN 查询直接 bind 数组——生成 WHERE id IN ('1,2,3') 既语法报错又绕过预处理
3、混用位置占位符和命名占位符——Db::query("", [1, ':status' => 0]) 中 :status 根本没被绑定但不报错
4、表名或字段名来自用户输入——这些不能参数绑定,必须白名单校验,否则可直接注入 DROP TABLE 等语句
5、生产环境忘记关闭调试模式——会暴露 SQL 语句和绑定参数,给攻击者提供调试信息
6、where(['field' => ['exp', "..."]]) 用法——该结构允许任意 SQL 片段注入,应删除所有此类调用
生产环境安全配置清单
除了代码层面的修复,部署时还需确认以下配置:
- 关闭调试模式:
app_debug必须设为 false - 关闭异常详情展示:防止报错信息泄露路径结构
- 数据库权限最小化:应用数据库账号仅授予必要的 CRUD 权限,禁止 DROP/ALTER 权限
- 定期更新框架版本:修复已知安全漏洞
- 开启 WAF 防护:在网关层拦截常见 SQL 注入特征