order by无法参数化,因数据库仅允许占位符绑定“值”而非“标识符”,强行使用?或#{field}会报语法错误;必须通过白名单严格校验字段名与排序方向。

Order By 注入无法用参数化修复,必须靠白名单校验——任何试图用 ORDER BY ? 或 ORDER BY #{field} 的写法都是错的。
为什么 ORDER BY 不能参数化?
数据库引擎不允许对排序字段、表名、别名等结构化语法成分做参数绑定。占位符 ? 或 #{} 只能代表“值”,不能代表“标识符”。一旦强行传入,会直接报错:ERROR: syntax error at or near "$1"(PostgreSQL)或 You have an error in your SQL syntax(MySQL)。
常见错误认知包括:
- 以为 MyBatis 的
#{}能兜底所有动态字段 —— 实际上它只对值安全,ORDER BY ${sortField}就是裸奔 - 在 Java 层用
String.format("ORDER BY %s", field)拼接 —— 等于把白名单逻辑甩给调用方,毫无约束 - 仅过滤单引号、分号、注释符 —— 攻击者可用反引号、方括号、Unicode空格绕过,且可能触发列名注入(如
user_name AS `status; DROP TABLE users;--`)
白名单映射必须严格到字段+方向+格式
只允许用户传一个索引或预定义键,后端查表映射成完整、带转义的排序子句。
推荐做法:
- 定义合法排序项为
Map<string string></string>,例如:{"created_at": "created_at ASC", "amount_desc": "amount DESC", "status": "status ASC"} - 拒绝任何未出现在该 Map 中的输入,包括空字符串、null、多余空格、大小写混用(如
CREATED_AT) - 若需支持升/降序切换,不拼接
ASC/DESC到字段名里,而是拆成两个独立键:"created_at_asc"和"created_at_desc" - 对字段名本身做最小化校验:只允许字母、下划线、数字,长度 ≤ 64,且不能以数字开头(防
123abc被误判为数字字面量)
MyBatis 中 ${} 的使用必须加双保险
如果业务强依赖 XML 动态排序(比如旧系统无法重构),${} 不可避免,但必须前置拦截:
- Controller 层收到
sort参数后,立即查白名单 Map,匹配失败直接throw IllegalArgumentException,不进 DAO - XML 中禁止出现
${sortField}这类裸变量,必须写成${safeSortClause},且该变量由 Service 层赋值(不是前端直传) - 避免在
<where></where>或<set></set>外部直接插${};尤其警惕ORDER BY ${field} ${direction}这种双变量拼接——direction同样要白名单校验 - 若用 MyBatis-Plus,禁用
QueryWrapper.orderBy(true, true, "user_name")这类接受任意字符串的方法,改用orderByAsc("user_name")等具名方法
导出场景下的特殊风险点
导出功能比普通列表更危险,因为攻击者有动机构造恶意排序触发数据泄露或服务崩溃:
- 排序字段若来自用户选列(如导出时勾选“部门名称”“上级主管”),这些列名也必须走同一套白名单,不能复用前端传来的原始字段名
- PostgreSQL 支持
ORDER BY 1, 2(按位置排序),若后端解析sort=1就直接拼ORDER BY 1,等于开放列位置枚举 —— 必须禁止数字位置排序,只认语义字段名 - 当排序字段含表达式(如
CONCAT(last_name, ', ', first_name)),白名单应明确列出整个表达式字符串,不可动态拼接 - 日志中记录排序参数时,脱敏处理:只记白名单 key(如
sort=created_at_desc),不记原始输入值
最易被忽略的是:白名单校验必须在事务开启前完成,且不能被缓存绕过。一旦漏掉某次请求的校验,就等于整条链路失守。











