导读:本期聚焦于马来西亚程序员创作的《如何规避MyBatis动态排序ORDER BY后的SQL注入?使用白名单机制过滤字段名可行吗》,敬请观看详情。在后台管理类系统里,前端往往要求用户自由切换列表的排序字段与顺序,MyBatis若直接把字段名拼进ORDER BY就会留下注入口。白名单机制是业界公认有效的防御手段,它要求服务端先定义允许排序的字段集合,任何请求携带的字段名都必须校验通过才拼入SQL。本文说明为何不能直接用${}接收排序参数,并给出基于Java集合与注解的两种白名单实现方式,比较各自适用场景与维护成本,同时提醒开发者注意排序方向也需要限定,避免asc、desc之外的字符引发异常或绕过。

MyBatis作为流行的持久层框架,常被用于拼接复杂查询语句。当业务需要前端动态指定排序字段与顺序时,不少开发者会在XML映射文件里使用ORDER BY ${sortField} ${sortOrder}这类写法。由于美元符号占位符不会做预编译参数绑定,而是把值直接替换进SQL文本,一旦字段名或排序方向来自未校验的用户输入,攻击者就能构造特殊字符串改变原意,形成SQL注入。例如传入sortField值为"update_time,(select 1 from dual where 1=2)"即可干扰查询逻辑。

如何规避MyBatis动态排序ORDER BY后的SQL注入?使用白名单机制过滤字段名可行吗

为什么ORDER BY不能使用#{}也无法直接信任${}

在MyBatis中,#{}会将参数转为JDBC的PreparedStatement占位符,由驱动做类型安全的绑定,这对WHERE条件里的字符串、数字非常有效。但ORDER BY后面的字段名属于SQL标识符,不是值,不能被引号包裹。如果写成ORDER BY #{sortField},多数数据库会将其解释为常量字符串而非列名,导致语法错误或排序失效。因此开发者容易退而求其次使用${},却忽视了它本质是字符串拼接。

使用${}接收排序字段时,MyBatis在解析阶段就把参数值原样写入映射后的SQL。假设接口暴露的排序字段由前端下拉框控制,看似只能选几个固定值,但接口参数可被抓包篡改。攻击者提交sortField=id;drop table user--就能尝试注入,虽然后端数据库账号权限可能限制危害,但任意SQL片段执行风险始终存在。排序方向参数同样危险,传入desc, (select sleep(5))也可能被拼入。

要解决这个矛盾,既不能丢掉动态排序的灵活性,又要避免标识符被篡改,核心思路是:所有进入ORDER BY的标识符都必须来自服务端受控集合,而不是用户原始输入。这就引出了白名单机制。

基于集合校验的纯Java白名单实现

最简单的白名单是在Java代码层维护一个允许排序的字段映射表,例如用Map关联前端传来的key与服务端真实的列名。这样即便用户提交未知key,也会因不在表中而被拒绝或回落到默认排序。该方法不依赖任何框架特性,清晰且易单测。

下面示例展示了一个排序参数解析工具,前端传field=name&dir=asc,后端先查白名单,命中才拼装。注意排序方向也限定为两个枚举,杜绝其他字符。

import java.util.HashMap;
import java.util.Map;

public class SortGuard {
    private static final Map<String, String> FIELD_WHITELIST = new HashMap<>();
    static {
        FIELD_WHITELIST.put("name", "user_name");
        FIELD_WHITELIST.put("age", "user_age");
        FIELD_WHITELIST.put("ctime", "create_time");
    }

    public static String buildOrderSql(String field, String dir) {
        String realField = FIELD_WHITELIST.get(field);
        if (realField == null) {
            realField = "create_time"; // 默认安全字段
        }
        String realDir = "desc".equalsIgnoreCase(dir) ? "desc" : "asc";
        return " ORDER BY " + realField + " " + realDir;
    }
}

上述代码把前端字段key映射为数据库列名,从根源上阻止了任意标识符注入,因为realField只能是白名单里的字符串字面量。即便攻击者传入field=xxx,也只会走到默认值。在Mapper调用时,可以把buildOrderSql结果作为一个整体参数用${}引入,但由于内容已受控,不再有注入风险。

这种写法的优点是直观、易调试,适合字段较少的系统。缺点是当表结构变更或新增排序项时,需要同步修改Java代码并重新发布。若团队希望把白名单声明贴近SQL,也可采用注解方案。

利用自定义注解与拦截器统一管控白名单

对于中大型项目,可以在Mapper方法参数上标记注解,声明该方法允许的排序字段范围,再通过MyBatis拦截器在参数入库前做校验。这样白名单随接口定义走,review时一眼可见,也方便集中审计。

示例定义一个@SafeSort注解,value为允许字段数组。拦截器扫描参数对象中的sortField属性,若不在数组内则抛异常。以下为简化版注解与拦截逻辑:

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SafeSort {
    String[] value();
}

// 拦截器片段
public Object intercept(Invocation inv) throws Throwable {
    Object[] args = inv.getArgs();
    // 假设第一个参数是包含sortField的Map
    if (args[0] instanceof Map) {
        Map<String, Object> param = (Map<String, Object>) args[0];
        String field = (String) param.get("sortField");
        Method m = inv.getMethod();
        SafeSort ann = m.getAnnotation(SafeSort.class);
        if (ann != null && !Arrays.asList(ann.value()).contains(field)) {
            param.put("sortField", "id"); // 回落默认
        }
    }
    return inv.proceed();
}

该方案把安全策略嵌入调用链,不需要每个排序逻辑都手写Map。配合XML里仍使用${sortField},只要拦截器保证sortField被净化即可。需要注意的是,拦截器应同时处理sortOrder,仅放行asc与desc。

从维护角度看,注解方式减少了重复代码,但当有多个Mapper且字段差异大时,注解值容易膨胀。团队可根据规模选择:小型系统用集合常量,复杂系统用注解加拦截器,甚至二者结合,把白名单抽取到配置中心实现热更新。

白名单机制的边界与补充防护

白名单能解决ORDER BY标识符注入,但并不意味着可以忽视其他动态SQL风险。例如WHERE条件中的LIKE、IN列表若用${}同样危险,应优先使用#{}或 <foreach> 标签。白名单只是针对标识符场景的补丁,不是万能药。

另外,即便用了白名单,也要防范排序字段对应的数据量过大导致全表排序拖垮数据库。建议对白名单字段建立索引,并在接口层限制分页大小。同时,前端传参应使用固定下拉而非自由文本,降低被探测可能。最后,定期开展代码审计,搜索所有${}用法,确认每个都经过校验,才能把SQL注入面缩到最小。

综合来看,使用白名单机制过滤字段名来规避MyBatis动态排序注入完全可行,也是目前最稳妥的做法。关键在于白名单必须由服务端定义、覆盖字段名与排序方向、并与默认安全值配合。只要落实这几点,动态排序功能就能在安全边界内平稳运行。

MyBatisSQL注入白名单机制修改时间:2026-08-18 22:32:42

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。