ThinkPHP防SQL注入核心在于查询写法:仅数组条件、三元语法、闭包查询真正触发PDO预处理绑定;字符串拼接如where("id=".input('id'))、原生SQL未用占位符+参数数组均绕过绑定,raw()、动态字段拼接及PDO模拟预处理也会失效。

ThinkPHP 防 SQL 注入,核心不在“用没用框架”,而在于“怎么写查询”。ORM 查询安全的前提是让变量不进 SQL 字符串;参数绑定真正起效,得靠 PDO 底层 prepare + bindValue 行为,不是日志里看到 ? 就算数。
ORM 查询中哪些写法真正触发参数绑定
只有明确把数据和 SQL 结构分离的写法,才会走 PDO 预处理流程:
- 数组条件:where(['id' => $id, 'status' => 1]) → 框架自动生成占位符并逐个 bindValue
- 三元语法:where('name', 'like', '%' . input('q') . '%') → like 操作符自动识别为参数绑定场景
- 闭包查询:where(function ($query) { $query->where('score', '>', input('min')); }) → 内部仍按数组/三元规则处理
反例:where("id = " . input('id'))、where("name like '%" . input('q') . "%'") —— 这些直接拼字符串,PDO 根本不介入,等于把输入原样交给数据库解析。
原生 SQL 必须手动绑定,且只认 ? 或命名占位符
Db::query() 和 Db::execute() 不自动识别变量,必须显式用占位符 + 数组传参:
立即学习“PHP免费学习笔记(深入)”;
- 位置占位符(?)配索引数组:Db::query("SELECT * FROM user WHERE status = ? AND level > ?", [1, 5])
- 命名占位符(:name)配关联数组:Db::query("SELECT * FROM user WHERE name = :name", [':name' => input('name')])
- 禁止混用:Db::query("...", [1, ':name' => 'admin']) —— 后者会被忽略
注意:? 不能用于表名、字段名、ORDER BY、GROUP BY 等结构部分,这些必须靠白名单校验,比如 in_array($order, ['id', 'create_time']) 才允许拼入。
危险但易被忽视的绕过点
有些写法看似用了框架方法,实则跳过绑定:
- Raw 表达式:where(['score' => ['exp', input('expr')]]) —— exp 表示“原样插入”,输入直接进 SQL
- 动态字段拼接:where($field . ' = ?', $value) —— ThinkPHP 5.1+ 默认禁用该语法,多数情况直接报错或退化为拼接
- PDO 模拟预处理开启:MySQL 低版本或 Docker 环境中,若 PDO::ATTR_EMULATE_PREPARES = true,prepare 实际仍是字符串替换
验证是否真绑定:开 debug + sql_explain,看日志中是否有 Binding: [123] 记录;更可靠的是用 Xdebug 跟到 Connection::execute(),确认调用了 $pdoStatement->bindValue()。
IN 查询和 LIKE 的安全写法
IN 和 LIKE 是高频注入点,错误写法极易失效:
- IN 安全做法:用链式查询 Db::name('user')->where('id', 'in', [1,2,3])->select() —— 框架内部已拆解为多个 ? 占位并绑定;如必须原生 SQL,则手动构造占位符串:$ids = [1,2,3]; $ph = str_repeat('?,', count($ids)-1) . '?'; Db::query("WHERE id IN ($ph)", $ids)
- LIKE 安全写法:where('title', 'like', '%' . input('kw') . '%') 或 where(['title' => ['like', '%' . input('kw') . '%']]) —— 框架识别 like 类型后自动绑定;别写 where("title like '%".input('kw')."%'")



















