动态排序字段必须通过白名单校验,因orderByAsc等方法底层拼接字段名而非预编译,易遭SQL注入;应使用SortColumnEnum枚举映射前端key与数据库字段,并校验sortOrder为ASC/DESC,XML中仅允许传校验后的${sortColumn}和${sortOrder}。

动态排序字段必须走白名单校验,否则用 ${sortField} 或 ${sortOrder} 就等于给攻击者开了个 SQL 注入后门。
为什么 QueryWrapper.orderByAsc(String column) 不能直接传前端参数
MyBatis-Plus 的 orderByAsc 等方法底层仍会把字段名拼进 SQL,不是预编译参数;它只对值做 #{} 绑定,字段名、表名、排序方向这些“SQL 结构部分”始终是字符串拼接。一旦前端传 sortField=update_time; DROP TABLE user;(或含换行/注释的变体),MP 内置的 replaceAllBlank 过滤不彻底,SqlInjectionUtils 也仅覆盖常见关键字,绕过方式太多。
- MP 的
OrderItem.setColumn()仅做空白字符替换,不校验语义合法性 - 正则检测有盲区:比如
upda/**/te_time、update%09time可能逃逸 - 即使字段名合法,
sortOrder若未限制为ASC/DESC,也可能注入ASC, (SELECT ...)
白名单枚举怎么写才真正防住注入
白名单必须同时约束字段名映射、默认方向、索引状态,并提供兜底逻辑。参考 SortColumnEnum 的写法:
public enum SortColumnEnum {
ID("id", "id", "DESC", true),
CREATE_TIME("createTime", "create_time", "DESC", true),
USER_NAME("userName", "user_name", "ASC", true);
<pre class="brush:php;toolbar:false;">private final String sortKey;
private final String dbColumn;
private final String defaultOrder;
private final boolean hasIndex;
// 构造与 getter 略
public static SortColumnEnum match(String sortKey) {
if (sortKey == null || sortKey.isBlank()) return ID;
for (SortColumnEnum e : values()) {
if (e.sortKey.equals(sortKey)) return e;
}
return ID; // 非法 key 兜底主键排序,不抛异常导致服务中断
}
public static String safeOrderDir(String dir) {
if ("asc".equalsIgnoreCase(dir)) return "ASC";
if ("desc".equalsIgnoreCase(dir)) return "DESC";
return null; // 返回 null,后续可统一 fallback 到默认方向
}
}
- 枚举值里
sortKey是前端传的 key(如createTime),dbColumn是真实数据库字段(create_time),二者必须分离,避免前端猜字段 -
match()方法返回SortColumnEnum实例,后续可直接取e.getDbColumn()和e.getDefaultOrder(),不依赖字符串拼接 -
safeOrderDir()返回null而非硬编码"ASC",方便上层判断是否需 fallback,默认方向已在枚举中定义好
XML 中如何安全使用白名单结果
在 XML 的 里,禁止直接插 ${sortField}。应将白名单校验后的字段和方向作为独立参数传入:
<select id="listUsers" resulttype="User">
SELECT * FROM user
<where><if test="status != null">status = #{status}</if></where><if test="sortColumn != null and sortOrder != null">
ORDER BY ${sortColumn} ${sortOrder}
</if></select>
- Mapper 接口调用时,传参必须是校验后的结果:
map.put("sortColumn", sortEnum.getDbColumn())、map.put("sortOrder", sortEnum.getDefaultOrder()) - 不能把
SortColumnEnum对象整个传进去再用 OGNL 表达式取字段——OGNL 在 XML 解析阶段执行,不受 Java 层白名单保护 -
${sortColumn}和${sortOrder}此时已是可信值,且范围被严格限定在枚举内,无注入风险
LambdaQueryWrapper 动态排序的坑与替代方案
别试图用 wrapper.orderByAsc("createTime") 直接传用户输入字段——虽然 MP 会做基础过滤,但该方法内部仍是字符串拼接,且无法控制排序方向的合法性。更糟的是,它破坏了类型安全:字段名写错不会编译报错。
- 正确做法:校验后用
orderByAsc+ 字段引用,例如wrapper.orderByAsc(User::getCreateTime),但前提是字段固定、不可动态切换 - 若真需动态多字段排序(如前端传
["createTime", "userName"]),必须先对每个字段调用SortColumnEnum.match(key),再用orderByAsc(e1.getDbColumn()).orderByDesc(e2.getDbColumn())拼接 - 注意:MP 的
orderByAsc(String... columns)重载方法不校验每个字段,只校验第一个,其余字段照单全收
白名单不是加个 if (allowedFields.contains(x)) 就完事。关键在于字段映射与数据库真实结构解耦、默认方向内置、非法输入有确定性兜底,且所有出口(XML / Wrapper / 自定义 SQL)都必须经过同一套校验逻辑。漏掉任意一环,注入风险就依然存在。











