ThinkPHP的where()传字符串不防SQL注入——因直接拼接SQL,跳过PDO绑定;必须用数组或闭包触发参数绑定,原生query需显式占位符绑定,raw()/exp()须白名单校验字段名。

where()传字符串等于直接拼SQL,别信“框架自动防护”
ThinkPHP 的 where() 方法**不防 SQL 注入**——只要传的是字符串且含用户输入,就等同于裸 SQL 执行。框架不会对 where("id = " . input('id')) 做任何转义或绑定,变量在 PHP 解析阶段就被展开,PDO 根本没机会介入。
常见错误写法:
where("name = '{$_GET['name']}'")where('status = ' . $status)where("title LIKE '%" . input('q') . "%")
正确替代方式必须触发 PDO 参数绑定:
where(['name' => input('name')])where('status', 'eq', $status)where(['title' => ['like', '%' . input('q') . '%']])
原生 query/execute 必须显式 bind,否则就是放行攻击
Db::query() 和 Db::execute() 是原生 SQL 接口,框架默认不干预。你传什么,它就扔给数据库什么。不显式绑定参数,等于放弃所有防护。
立即学习“PHP免费学习笔记(深入)”;
危险写法(哪怕加了单引号):
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 子句不能直接 bind([1,2,3]),必须动态生成占位符:
$ids = [1, 2, 3]; $placeholders = str_repeat('?,', count($ids) - 1) . '?'; Db::query("SELECT * FROM user WHERE id IN ($placeholders)", $ids);
raw()/exp() 是免死金牌,但用错就真没救
raw()(TP6)和已废弃的 exp()(TP5)本质是告诉框架:“这段我来负责,你别管”。一旦用了,框架跳过所有绑定、转义、类型检查,原样插入 SQL。
典型误用:
where('created_time > ' . raw(input('start')))order(raw(input('sort')))
正确做法:排序字段、表名、字段名这类非值上下文,必须白名单校验:
in_array(input('sort'), ['id', 'name', 'create_time']) ?: 'id'
如果真要用 raw(),只限绝对可信来源:后台配置项、硬编码枚举映射、服务端固定值。
模拟预处理和二次注入最容易被忽略
PDO 的 PDO::ATTR_EMULATE_PREPARES 设为 true 时,预处理会被退化为字符串拼接——尤其在 MySQL 低版本或 Docker 环境中极易触发。这会让看似安全的数组写法实际失效。
必须在数据库连接配置中显式关闭:
'PDO::ATTR_EMULATE_PREPARES' => false
二次注入更隐蔽:从数据库读出一条带恶意 payload 的数据(比如昵称字段存了 1 OR 1=1),再拿它拼进新查询,等于把后门二次释放。
验证是否真走绑定,不能只看日志里有没有 ?,得确认底层是否调用了 bindValue() 或 MySQL 日志中是否出现 WHERE id = ? 而非 WHERE id = 123。



















