ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Mybatis动态排序攻击面-ORDER-BY注入与字段白名单

Mybatis动态排序攻击面-ORDER-BY注入与字段白名单 动态排序攻击面ORDER BY 注入与字段白名单攻击面查询值可以使用预编译参数表名和排序列却不能直接用?占位。为了支持前端表格排序不少项目把sortField原样拼进${}由此留下 SQL 注入、越权探测和不可控慢查询风险。本文给出枚举白名单、字段映射和类型安全方法引用三层方案并结合 MetaLiteOrderBy的源码说明公共查询组件应该负责什么、不能相信什么。很多后台表格都会提交这两个参数{sortField:createdTime,sortDirection:desc}为了生成动态排序一些 MyBatis XML 会写成ORDER BY ${sortField} ${sortDirection}功能上线很快但${}是字符串替换不会像#{}一样变成预编译参数。只要字段来自外部输入攻击者就可能改变 SQL 结构。一、为什么排序列不能使用普通占位符下面这种写法通常不能表达动态列名ORDERBY?DESC数据库会把?当成一个值而不是 SQL 标识符。列名、表名、排序方向属于 SQL 结构必须在生成 SQL 前完成可信映射。这也是动态排序比普通查询参数更危险的原因user_id ?的值不会改变 SQL 结构ORDER BY ${field}会直接改变解析后的语句。二、风险不只有传统 SQL 注入即使数据库驱动禁止多语句执行动态排序仍可能带来其他问题。1. 探测内部字段攻击者不断尝试字段名可以推断表结构、隐藏列或数据库函数是否可用。2. 构造高成本表达式复杂函数、计算表达式或未建索引字段可能让分页查询突然变慢。3. 绕过数据展示规则某些字段虽然允许查询却不应该成为公开排序条件。例如内部风险分、逻辑删除标记或安全等级。4. 排序结果不稳定只按非唯一字段排序翻页时可能出现重复或遗漏记录。三、最稳妥的入口是业务白名单前端参数不应直接等于数据库字段而应先映射到服务端枚举enumUserSortField{CREATED_TIME,USERNAME,STATUS}Service 再把枚举转换为受信字段OrderByorderByswitch(param.getSortField()){caseCREATED_TIME-OrderBy.desc(UserEntity::getCreatedTime);caseUSERNAME-OrderBy.asc(UserEntity::getUsername);caseSTATUS-OrderBy.asc(UserEntity::getStatus);};这样前端只能选择产品允许的排序能力无法提交任意数据库标识符。四、MetaLite 如何把字段引用转换成列名MetaLiteOrderBy支持字符串字段OrderBy.desc(createdTime)也支持 Java 方法引用OrderBy.desc(UserEntity::getCreatedTime)方法引用版本通过EntityHelper.genFieldName(function)提取实体属性publicstaticT,ROrderBydesc(EntityFieldNameFunctionT,Rfunction){AssertUtil.paramNotNull(function,function);returnnewOrderBy(EntityHelper.genFieldName(function),Direction.DESC);}JDBC SQL 生成阶段再从实体属性到数据库列名映射表中取值sb.append(property2ColumnMap.get(orderBy.getKey())).append( ).append(orderBy.getDirection());这里体现了两层约束排序方向来自框架枚举只能是受支持的值方法引用必须指向实体真实属性字段改名时编译器能够发现问题。五、方法引用不等于前端输入已经安全如果 Controller 仍然接收任意字符串然后调用OrderBy.desc(param.getSortField())虽然property2ColumnMap不一定能映射非法字段但这不是完整的安全契约。调用方仍应校验字段是否在当前接口白名单排序方向是否合法字段是否适合建立排序索引是否需要追加稳定的主键排序。公共 ORM 可以提供类型安全能力却无法判断某个字段是否应该开放给某个页面。六、分页排序必须增加唯一兜底字段假设大量订单的创建时间完全相同ORDERBYcreated_timeDESCLIMIT20OFFSET20数据库对相同时间记录的内部顺序没有承诺。并发插入后第二页可能重复出现第一页数据也可能漏掉记录。建议写成query.orderBy(OrderBy.desc(OrderEntity::getCreatedTime),OrderBy.desc(OrderEntity::getId));其中id负责形成确定的全序。七、复杂排序应该怎样处理真实业务可能需要聚合别名排序距离排序自定义状态优先级跨表字段排序数据库函数计算排序。这类排序不应为了复用通用列表接口而开放任意表达式。更合适的做法是为场景定义专用查询方法SQL 表达式固定在服务端外部只传有限的策略枚举对慢排序建立独立索引和执行计划测试。MetaLite 的字符串OrderBy是扩展逃生口不应该成为外部参数直通 SQL 的入口。八、可直接复用的动态排序检查清单Controller 不直接接收数据库列名外部字段先映射为服务端枚举排序方向只允许ASC、DESCORM 内优先使用实体方法引用分页排序追加唯一主键公开排序字段有索引和慢 SQL 验证聚合、函数和跨表排序使用专用查询日志记录最终排序策略但不回显内部表结构。九、结论动态排序的本质不是“拼一个 ORDER BY”而是把外部意图转换为受控的 SQL 结构。MetaLite 方法引用解决了字段改名和内部类型安全问题业务白名单解决了哪些能力允许对外开放的问题稳定排序和索引验证解决了分页正确性与性能问题。三层缺一不可。框架简介MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。源码基线JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3具体组件版本以项目backend-bom为准。作者简介15 年 Spring 体系企业级开发经验专注于 Java 微服务架构、工程治理与生产实践。持续更新MetaLite 系列内容将持续更新围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者及时获取后续内容。在线演示演示地址: https://admin.metalite.top/演示账号: guess演示密码: admin2026
返回列表