必须使用白名单校验排序字段和方向,再以数组形式传入order(),禁止字符串拼接或db::raw()直传用户输入;分页需appends透传参数,缓存需包含排序字段。

ThinkPHP 模型查询时怎么动态指定排序字段
不能直接把用户传来的字段名拼进 order(),否则会 SQL 注入。ThinkPHP 的 order() 方法本身不校验字段合法性,传入恶意字符串(比如 id, (SELECT password FROM think_user LIMIT 1))会直接执行。
正确做法是:先白名单过滤,再传入 order()。不是“支持任意字段”,而是“只支持你明确允许的字段”。
- 定义一个可排序字段白名单数组,比如
['id', 'create_time', 'sort', 'price'] - 用
in_array()判断用户请求的sort_field是否在白名单中 - 同时检查排序方向(
asc/desc),只接受这两个值,其他一律 fallback 到asc - 构造
order()参数时,用数组形式:[$field => $direction],避免字符串拼接
示例:
$allowFields = ['id', 'create_time', 'price', 'sort'];
$field = input('sort_field', 'id');
$dir = in_array(input('sort_dir', 'asc'), ['asc', 'desc']) ? input('sort_dir') : 'asc';
if (!in_array($field, $allowFields)) {
$field = 'id';
}
$list = Db::name('goods')->order([$field => $dir])->select();
为什么不能用字符串拼接写 order('xxx '.input('dir'))
因为 order() 接收字符串时,ThinkPHP 不做任何字段名转义或关键字过滤,相当于裸 SQL 片段插入。一旦用户传入 sort_field=id; DROP TABLE think_user;(虽然实际执行受限于 MySQL 协议,但多语句或子查询仍可能触发),风险就不可控。
更隐蔽的问题是:某些字段名含下划线、大小写混合(如 user_name),或本身就是保留字(如 order、group),字符串拼接后没加反引号,会导致 SQL 报错或逻辑错乱。
- ThinkPHP 6+ 的
order()数组用法会自动给字段加反引号,字符串用法则不会 - MySQL 严格模式下,未加反引号的保留字字段直接报错:
ERROR 1064 (42000): You have an error in your SQL syntax - 即使字段名合法,方向参数若没过滤,传入
desc, sleep(5)这类也能绕过基础校验
使用 Db::raw() 动态拼接排序字段的风险与替代方案
有人想用 Db::raw() 绕过白名单,比如 order(Db::raw(input('field').' '.input('dir'))) —— 这等于把闸门全打开,比字符串拼接更危险,因为 Db::raw() 明确告诉框架“别处理,原样上”。绝对不要这么做。
真正需要灵活字段(比如关联表字段、JSON 字段提取)时,应走另一条路:预定义规则 + 映射转换。
- 用户传
sort_field=price_min,后端映射为JSON_EXTRACT(price_range, "$.min") - 用户传
sort_field=author_name,后端关联think_user表并指定user.nickname - 所有映射逻辑写死在代码里,不暴露原始字段名,也不接受任意表达式
- 映射后的 SQL 片段仍需经
Db::raw(),但来源可控,非用户直输
例如:
$map = [
'price_min' => Db::raw('JSON_EXTRACT(price_range, "$.min")'),
'author_name' => 'user.nickname',
];
$field = $map[input('sort_field', 'id')] ?? $map['id'];
$list = Db::name('goods')->alias('g')
->join('think_user user', 'g.user_id = user.id')
->order($field . ' ' . ($dir === 'desc' ? 'DESC' : 'ASC'))
->select();
分页 + 动态排序时容易漏掉的坑
排序字段变化后,如果不把当前排序参数也带进分页链接,翻页时会回到默认排序(比如 id),用户会以为数据错乱。这不是 ThinkPHP 的 Bug,是开发者没显式透传参数。
- 用
paginate()时,通过->appends()手动追加sort_field和sort_dir - 注意
appends()传的是键值对,不是 URL 字符串,别写成appends('sort_field=price') - 如果用了路由变量(如
GET /list/:sort_field/:sort_dir),要确保分页生成的链接能正确识别这些变量 - 缓存场景下,不同排序参数必须生成不同缓存 key,否则
price desc的结果可能被id asc覆盖
示例:
$paginate = Db::name('goods')
->order([$field => $dir])
->paginate(['appends' => ['sort_field' => $field, 'sort_dir' => $dir]]);
字段名白名单和排序方向校验这两步,少一个都可能让整个排序功能变成注入入口。很多人测的时候只看“能排”,不看“能不能被利用”,上线后才出问题。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










