ORDER BY无法参数化是因SQL标准禁止占位符替代列名,数据库将?视为字符串常量而非字段名,导致排序失效或报错;必须用${}拼接但需白名单严格校验字段名和排序方向。

ORDER BY 后面写 ? 会直接报错或排序失效
MySQL、PostgreSQL 等数据库语法层就不允许在 ORDER BY 后用占位符(如 ? 或 $1)代替列名。这不是 MyBatis 或驱动的缺陷,是 SQL 标准决定的:列名属于语法结构,不是运行时值。
你写 SELECT * FROM user ORDER BY ? 并绑定 "id",数据库不会按 id 列的值排序,而是把字符串字面量 "id" 当作常量——所有行都“排在同一位置”,结果无序且难以察觉。PostgreSQL 甚至直接报错:syntax error at or near "$1"。
MyBatis 的 #{sortField} 在 ORDER BY 中同样无效,它会被加上引号变成 ORDER BY "id",而双引号在 MySQL 中是语法错误,在 PostgreSQL 中则表示标识符引用——但若字段不存在,照样报错或静默失败。
MyBatis-Plus 的 orderByAsc(String) 不等于参数化
QueryWrapper.orderByAsc("name") 看似安全,是因为你写了硬编码字符串;一旦换成 QueryWrapper.orderByAsc(request.getParameter("sort")),就等同于字符串拼接——内部没走 #{}<code> 绑定,而是直接插入生成的 SQL。
攻击者传 sort=name ASC, (SELECT password FROM user LIMIT 1),最终 SQL 就是:ORDER BY name ASC, (SELECT password FROM user LIMIT 1),完全绕过所有 ORM 层过滤。
常见误判点:
-
${sortField}在 XML 中使用,等于裸拼,MyBatis 不做任何拦截 -
PageHelper.orderBy("name DESC")若参数来自用户输入,同样高危 -
example.setOrderByClause(...)是 MyBatis 原生方式,内容直插 SQL,必须前置校验
白名单必须同时覆盖字段名和排序方向
只校验字段名(如 in_array($f, ['id', 'name'])),放行任意 sortOrder,攻击者就能构造 name ASC, (SELECT @@version) 这类 payload。
真正有效的白名单是二维控制:
- 字段名:仅允许
['id', 'created_at', 'status', 'amount'],且需trim()去首尾空格,防" id "逃逸 - 排序方向:严格枚举
['asc', 'desc'],并统一转小写比对(PHP 要加true参数防弱类型比较) - 组合校验:不推荐拼接
${field} ${dir},应预定义映射表,如["created_at_asc" => "created_at ASC"],前端只传键名
Java 可用 enum SortField { ID, CREATED_AT, STATUS } + enum SortOrder { ASC, DESC },拒绝任何非枚举值。
XML 层校验比 Java 层更可靠
Controller 里写 if (!ALLOWED_FIELDS.contains(sort)) throw 看似合理,但可能被反射调用、AOP 绕过、或中间件篡改参数跳过校验。
MyBatis OGNL 表达式在 SQL 构建前执行,天然更早介入:
- ✅ 安全写法:
<if test="sortField == 'name' && sortOrder == 'asc'">ORDER BY name ASC</if> - ❌ 危险写法:
<otherwise>ORDER BY ${sortField} ${sortOrder}</otherwise>—— 等于放弃防御 - ⚠️ 注意:
test中不能用方法调用(如contains()),得靠显式枚举或bind预处理
最易被忽略的是:ORDER BY 注入往往不报错、不中断流程,攻击者能静默探测表结构或提取敏感字段,问题长期潜伏。

















