ThinkPHP默认使用预处理,但需避免字符串拼接SQL;正确用数组、占位符或键值对触发绑定;input()仅防XSS不防SQL注入;JSON请求需手动校验;Db::query()必须用占位符才安全。

ThinkPHP 没有“开启预编译”的开关——它默认就用预处理(prepared statement),关键在于你有没有无意中绕过它。
where() 传字符串时必须手动触发预处理
很多人以为 where('id='.$id) 也能防注入,其实完全不能。这种写法等于把用户输入直接拼进 SQL 字符串,PDO 预处理根本没机会介入。
正确做法是让框架识别出这是需要参数绑定的场景:
- 用数组条件:
where(['id' => $id])—— 自动走类型检测 + 绑定 - 用占位符字符串:
where('id=%d', $id)或where('id=:id', [':id' => $id])—— 显式启用预处理 - 用键值对形式:
where('id', $id)—— 查询构造器自动绑定,最推荐
错误示例:where("id=".$_GET['id'])、where("id=".$_POST['id']),这类写法在任何 ThinkPHP 版本里都等同于裸奔。
立即学习“PHP免费学习笔记(深入)”;
input() 和 I() 不等于安全,只是入口过滤
input('get.id') 或旧版 I('get.id') 默认只做 htmlspecialchars,对 SQL 注入零防护。它只影响 XSS,不碰数据库层。
真正起作用的是后续怎么用这个值:
- 如果拿去拼 SQL:
"SELECT * FROM user WHERE id=".$id→ 危险 - 如果喂给查询构造器:
where('id', $id)→ 安全(绑定生效) - 如果喂给原生
query():query('SELECT * FROM user WHERE id=?', [$id])→ 安全
别迷信 input() 的第三个参数加 intval 就万事大吉——若你后面又把它转成字符串再拼 SQL,前面的过滤就白做了。
JSON 请求体里的数据最容易漏防
ThinkPHP 默认对 Content-Type: application/json 请求自动调用 json_decode,但不做任何键名校验或深度限制。
攻击者可传:{"name":"x","__destruct":"y"} 或超深嵌套对象,一旦你用 array_merge、extract 或模型 save() 直接接收,就可能触发反序列化或内存耗尽。
必须手动加固:
- 关掉自动解析:
'json_bind' => false(在app.php配置) - 手动解码加约束:
json_decode($raw, true, 512, JSON_THROW_ON_ERROR) - 立即过滤魔术键:
array_filter($data, function($k) { return strpos($k, '__') !== 0; }, ARRAY_FILTER_USE_KEY) - 批量操作前逐层校验:
saveAll()不会帮你扫子数组里的__construct
Db::query() 和 execute() 是双刃剑
它们支持预处理,但前提是——你得主动用占位符或数组传参。写成 Db::query("SELECT * FROM user WHERE id = $id") 就和裸 mysql_query 一样危险。
安全写法只有两种:
Db::query("SELECT * FROM user WHERE id = ?", [$id])Db::query("SELECT * FROM user WHERE id = :id", [':id' => $id])
注意:ThinkPHP 3.2.3 的 query() 不支持 ? 占位符,只认 :name 形式;TP5/6 才全面支持 ?。版本差异不看清,写了也白写。
最常被忽略的一点:预处理只保 SQL 层,不保业务逻辑。比如你用 where(['status' => input('get.status')]),而用户传 status[eq]=1,数组结构可能绕过类型判断——这时候靠的是配置 'DB_PARAMS'=>['strict'=>true] 或代码层强制整形,不是预处理本身。



















