ThinkPHP模型排序必须使用白名单校验字段和方向,禁用直接传用户输入;推荐用parseOrder()配合schema,关联字段需手动映射别名并确保JOIN正确。

ThinkPHP模型排序参数不能直接传用户输入
直接把 $_GET['sort'] 或 $request->param('sort') 塞进 order() 会导致 SQL 注入,比如传 id ASC, (SELECT SLEEP(5)) 就能拖慢甚至瘫痪数据库。ThinkPHP 的 order() 默认不校验字段名合法性,只做字符串拼接。
- 合法排序字段必须白名单预定义,不能靠正则“过滤掉危险字符”——因为
id` DESC, user_id ASC这类绕过正则的写法依然有效 - 字段名和排序方向(
ASC/DESC)要分开校验,不能只校验字段名而放行任意方向值 - 使用
orderRaw()更危险,它默认跳过所有安全机制,除非你手动调用parseOrder()
用 parseOrder() 安全校验并转换排序参数
ThinkPHP 6.0+ 提供了 parseOrder() 方法,它会检查字段是否在模型的 $schema 或数据库表结构中存在,并自动转义方向关键词。但注意:它只对「字段名 + 方向」组合生效,不接受原始 SQL 片段。
- 必须先调用
$model->getSchema()或确保模型已定义$schema属性,否则parseOrder()会退化为简单白名单匹配 - 传入格式应为数组:
['create_time' => 'desc', 'id' => 'asc'],而非字符串'create_time desc, id asc' - 如果字段不在 schema 中(比如关联查询的
user.name),需手动扩展白名单,不能依赖自动识别
// 正确用法
$allowFields = ['id', 'name', 'create_time', 'status'];
$sort = $request->param('sort', 'id');
$direction = strtolower($request->param('dir', 'asc'));
if (!in_array($sort, $allowFields) || !in_array($direction, ['asc', 'desc'])) {
throw new HttpException(400, 'Invalid sort parameter');
}
$list = User::order($sort, $direction)->select();
关联字段排序必须显式白名单 + 别名映射
想按 User.address.city 排序?parseOrder() 不识别关联字段路径。TP 不会自动解析 address.city 对应的表别名或 JOIN 条件,直接传进去会被当成主表字段,查不到或报错。
- 必须提前在控制器里写死映射关系,例如
['city' => 'address.city', 'province' => 'address.province'] - JOIN 查询时,确保关联表已用
alias()设定别名(如->alias('u')->join('address a', 'u.id = a.user_id')),再用别名拼字段 - 禁止拼接用户输入到
orderRaw(),哪怕加了addslashes()或htmlspecialchars()—— 这些对 SQL 层完全无效
分页 + 动态排序时 URL 参数要同步透传
用 paginate() 后,翻页链接不会自动携带当前 sort 和 dir,导致第二页回到默认排序。这不是安全问题,但会让前端体验断裂,间接引发用户反复提交非法参数试探。
立即学习“PHP免费学习笔记(深入)”;
- 分页方法要显式传入
query参数:->paginate(['query' => request()->param()]) - 但注意:
request()->param()包含所有参数,包括可能被污染的sort。应在传入前先做过滤,比如只取已校验过的键 - 前端生成排序链接时,不要用
location.href += '&sort=xxx'拼接,应完整重写 URL 参数,避免重复或冲突
parseOrder() 看似省事,但没 schema 支持就形同虚设。



















