ThinkPHP本身不自动防SQL注入,安全取决于是否触发PDO绑定;必须用数组或闭包写where条件,禁字符串拼接,原生SQL需显式绑定,禁用raw/exp,关闭模拟预处理,并通过日志和Xdebug验证绑定真实生效。

ThinkPHP 本身不自动防 SQL 注入,安全与否取决于你是否让参数真正走 PDO 绑定。只要用户输入进了字符串拼接的 SQL,哪怕只有一处,风险就已存在。
where 条件必须用数组或闭包
ThinkPHP 只对数组格式(如 where(['id' => $id]))和闭包格式(如 where(function($q) { $q->where('name', $name); }))启用自动参数绑定。其他写法都会跳过防护。
- ❌ 危险写法:
where("id = " . input('id'))、where("name LIKE '%" . input('kw') . "%')——变量直插,无任何转义 - ✅ 安全写法:
where(['id' => input('id', 0, 'intval')])、where(['name' => ['like', '%' . input('kw') . '%']]) - 字段名、排序字段不能由用户直接控制,如
order(input('sort')),需白名单校验:in_array(input('sort'), ['id', 'create_time'], true) ? input('sort') : 'id'
原生 SQL 必须显式绑定参数
Db::query() 和 Db::execute() 是“裸接口”,框架默认不做任何处理。不传参数数组,等于主动放弃防护。
- ❌ 危险写法:
Db::query("SELECT * FROM user WHERE name = '" . input('name') . "'") - ✅ 安全写法(位置占位):
Db::query("SELECT * FROM user WHERE name = ? AND status = ?", [input('name'), 1]) - ✅ 安全写法(命名占位):
Db::query("SELECT * FROM user WHERE name = :name", [':name' => input('name')]) - IN 子句要动态生成占位符:
$ids = [1,2,3]; $ph = str_repeat('?,', count($ids)-1) . '?'; Db::query("WHERE id IN ($ph)", $ids)
禁用危险接口与高危配置
有些功能设计上就绕过绑定,仅限内部可信逻辑使用,绝不可暴露给用户输入。
立即学习“PHP免费学习笔记(深入)”;
-
raw()和exp()是“原样插入”指令,如where(['id' => ['exp', "1 OR 1=1"]])直接注入,应彻底禁用或加运行时断言 - PDO 模拟预处理(
PDO::ATTR_EMULATE_PREPARES => true)会让 prepare 退化为字符串拼接,在 MySQL 5.6 或 Docker 环境中极易触发,必须设为false - 数据库连接配置中强制指定字符集:
'charset' => 'utf8mb4',防止宽字节注入
验证绑定是否真实生效
光看代码写法不够,得确认底层 PDO 真正调用了 bindValue(),而不是日志里显示 WHERE id = ? 就以为安全了。
- 开启调试模式:
'debug' => true, 'sql_explain' => true,检查日志中是否有Binding: [123]记录 - 在 MySQL 服务端开启
general_log,观察执行语句是WHERE id = ?还是WHERE id = 123——后者说明已被模拟或跳过绑定 - 用 Xdebug 断点跟踪到
think\db\Connection::execute(),确认$pdoStatement->bindValue()被调用



















