ThinkPHP5默认仅对数组和闭包形式的where()启用PDO参数绑定,字符串拼接、raw()/exp()调用、动态字段名、未校验的order/group、IN查询未动态占位及PDO模拟预处理开启均会导致防御失效。

ThinkPHP5 默认对数组和闭包形式的 where() 调用启用 PDO 参数绑定,但只要出现字符串拼接、raw()、exp() 或动态字段名,防御就立即失效——这不是“有没有防”,而是“你有没有主动破坏它”。
字符串 where 条件是最大雷区
看似加了引号或格式化符,实际等于裸奔。ThinkPHP5 的 where("id = " . input('id')) 或 where("name LIKE '%{input('kw')}%'") 完全跳过预处理,变量直接进入 SQL 解析流程。
-
where()接字符串时,框架只做最基础的real_escape_string(MySQLi)或模拟转义(PDO 模拟模式下),无法防1' OR '1'='1这类绕过 - TP5.1 支持
%d/%s占位语法(如where("id=%d AND status=%s", [$id, $status])),但仅限where()、query()、execute()三个方法,且必须严格匹配参数数量与类型 - 别信
"id='" . (int)$id . "'"这种写法——如果$id来自$_GET['id']且未过滤,攻击者可传id[]=1触发 PHP 数组转字符串为Array,导致 SQL 语法错误并暴露路径
原生 SQL 必须显式 bind(),不能靠 “看起来安全”
Db::query() 和 Db::execute() 是纯透传接口,不自动绑定、不自动转义、不校验参数。你传什么,PDO 就执行什么。
- 错:
Db::query("SELECT * FROM user WHERE name = '" . input('name') . "'")—— 单引号毫无意义,PHP 字符串拼接已提前完成 - 对(位置占位):
Db::query("SELECT * FROM user WHERE name = ? AND status = ?", [input('name'), input('status')]) - 对(命名占位):
Db::query("SELECT * FROM user WHERE name = :name", [':name' => input('name')]) - IN 查询不能直接传数组:
Db::query("WHERE id IN (?)", [[1,2,3]])会生成IN ('1,2,3');必须手动生成占位符:$ids = [1,2,3]; $in = str_repeat('?,', count($ids) - 1) . '?'; Db::query("WHERE id IN ($in)", $ids)
raw() 和 exp() 不是高级功能,是紧急逃生舱
这两个方法本质是告诉框架:“这段 SQL 我自己负责,你别插手”。一旦调用,所有绑定、转义、白名单逻辑全部失效。
立即学习“PHP免费学习笔记(深入)”;
-
where('price', 'exp', input('price'))若input('price')是"1 OR 1=1",最终 SQL 就是price = 1 OR 1=1 -
order(raw(input('sort')))是典型排序注入点,sort=id; SELECT SLEEP(5)--可直接触发延时盲注 - 替代方案不是禁用,而是白名单校验:
$sort = in_array(input('sort'), ['id', 'create_time', 'username']) ? input('sort') : 'id'; order($sort) - 若真需
raw()(如 JSON_EXTRACT 字段查询),输入必须来自后台配置或枚举映射,绝不能来自用户请求体
模型层字段可控性比 SQL 更危险
很多人盯着 where 防注入,却忽略 save()、update() 时字段名本身也可被操控。ThinkPHP5 允许通过 force 参数指定任意字段写入,配合用户可控的 key,等于开放 SQL 注入后门。
- 错:
$model->data($_POST)->save(),若$_POST包含['username' => 'admin', 'status' => '1', 'xxx' => '1 OR 1=1'],且模型未设protected $field白名单,xxx会被当作字段插入 - 必须在模型中声明
protected $field = ['username', 'email', 'status'];,并开启protected $autoWriteTimestamp = true;等强约束 -
allowField()方法要重写,拒绝任何不在白名单中的字段:return parent::allowField($field) && in_array($field, $this->field); - 别依赖
validate或filter,它们只校验值,不校验字段名本身是否合法
最易被忽略的是 PDO 模拟预处理开关——PDO::ATTR_EMULATE_PREPARES => false 必须显式配置,否则 MySQL 低版本或 Docker 容器里,? 占位会退化为字符串拼接,日志里看着像绑定了,其实没绑。



















