ThinkPHP 如何防止 SQL 注入攻击最佳实践

文章导读
ThinkPHP 防止 SQL 注入的核心是强制使用参数绑定,where 条件用数组或闭包,原生 SQL 必须显式绑定占位符,字段名类参数走白名单校验。
📋 目录
  1. 核心原理
  2. 分步处理
  3. 怎么验证是否生效
  4. 常见坑
  5. 生产环境安全配置清单
A A

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);

更省心的方式是使用链式调用:

ThinkPHP 如何防止 SQL 注入攻击最佳实践

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 注入特征