Skip to content

fix(mgr): 修复角色列表排序字段缺少校验导致的注入风险 - #146

Open
rwzdk wants to merge 2 commits into
jeequan:devfrom
rwzdk:fix/role-list-sort-field-validation
Open

rwzdk wants to merge 2 commits into
jeequan:devfrom
rwzdk:fix/role-list-sort-field-validation

Conversation

@rwzdk

@rwzdk rwzdk commented Oct 26, 2026

Copy link
Copy Markdown

问题与验收标准

  • 关联 Issue(小修复可直接描述):SQL Injection in Jeepay Manager Role-List sortField #118
  • 问题 / 需求与预期结果:
    • 现象:运营端「角色管理」列表接口 GET /api/sysRoles 的排序字段由前端参数 sortField 指定。后端拿到该参数后只做驼峰转下划线的格式转换,就直接拼接进 SQL 的 ORDER BY 子句,没有做任何字符或字段校验。传入非普通标识符的内容时,它会被当作 SQL 片段而不是列名处理(详见 SQL Injection in Jeepay Manager Role-List sortField #118)。
    • 根因:
      1. jeepay-core/.../ctrls/AbstractCtrl.java 的 getSortInfo() 只做格式转换(CharSequenceUtil.toUnderlineCase),没有任何白名单校验,非法值原样透传;
      2. jeepay-manager/.../ctrl/sysuser/SysRoleController.java 的 list() 把该值直接交给 condition.orderBy(...) 拼进 ORDER BY,且没有「角色表可排序字段」的白名单;
      3. 顺带发现 getSortInfo() 的空值判断写成了 StringUtils.isAllEmpty(sortField, sortField)(同一参数传了两次)。
    • 预期:排序字段只允许标准 SQL 标识符;角色列表只允许按角色表的白名单字段排序,其余回退到默认排序,不把任何外部传入的字符串拼进 ORDER BY。
  • 本次范围与不涉及的内容:
    • 只改 jeepay-core 的排序参数解析与 jeepay-manager 的角色列表排序,补回归测试。
    • 不涉及商户端、数据结构、配置、菜单与权限体系;getSortInfo() 在业务代码中只有角色列表这一处调用(grep 确认),影响面限于该接口。

改动与兼容性

  • 实现要点:
    • AbstractCtrl.getSortInfo():新增 SORT_FIELD_PATTERN(^[A-Za-z_][A-Za-z0-9_]{0,63}$),对排序字段 trim 后校验,只放行标准 SQL 标识符;不合法时抛 BizException(ApiCodeEnum.PARAMS_ERROR, "排序字段不合法")。同时把重复传参的空值判断修正为 StringUtils.isBlank(sortField)。
    • SysRoleController.list():新增可排序字段白名单 role_id / role_name / updated_at;命中白名单才使用调用方指定的列与方向(改用语义明确的 orderByAsc / orderByDesc),未命中则回退到原有默认排序 updated_at DESC,不再拼接外部传入的字符串。
    • 生产代码改动共 2 个文件、+21/−3,其余为新增测试。
  • 配套前端 / Skills PR 或版本(无则写不涉及):不涉及,前端未改动。
  • API、权限、签名、数据库 / 配置影响与升级、回滚说明:
    • 接口路径、参数、响应结构与权限标识不变。行为变化只有两处,均为收紧:非法排序字段由「拼进 SQL」改为返回参数错误;白名单外的字段由「按调用方指定的列排序」改为「按 updated_at DESC 默认排序」。
    • 原本可用的排序字段 role_id / role_name / updated_at(以及对应的驼峰写法 roleId / roleName / updatedAt)全部保留。
    • 无数据库结构、配置与部署变更;回滚只需还原这两个文件的改动。

验证

  • 命令 / 人工步骤与结果:
    • mvn -pl jeepay-core,jeepay-manager -am test:BUILD SUCCESS
      • jeepay-core:Tests run: 14, Failures: 0, Errors: 0, Skipped: 0
      • jeepay-manager:Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
    • 全量 mvn -B test:jeepay-core 14、jeepay-components-mq 4、jeepay-manager 9、jeepay-merchant 29,全部通过,BUILD SUCCESS。
    • 本地环境:JDK 21 + Maven 3.9.16(项目 java.version=17,仓库 CI 使用 JDK 17)。
  • 新增回归测试(修复前失败、修复后通过;无法自动化请说明):
    • 新增 jeepay-core/src/test/java/com/jeequan/jeepay/core/ctrls/AbstractCtrlSortFieldTest.java:覆盖非法排序字段被拒、合法驼峰字段仍映射为下划线列名、升序标记保留、排序字段缺省时返回 null。
    • 新增 jeepay-manager/src/test/java/com/jeequan/jeepay/mgr/ctrl/sysuser/SysRoleControllerSortFieldTest.java:直接捕获 MyBatis-Plus 生成的 ORDER BY 片段,断言白名单字段按请求方向排序、非白名单字段回退到默认排序、带 SQL 元字符的字段被拒绝且不查库。纯单元测试,不需要数据库、Redis 或真实支付渠道。
    • 修复前(只回退两个生产文件的改动、保留测试):
      $ mvn -pl jeepay-core -am test -Dtest=AbstractCtrlSortFieldTest
      [ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0 <<< FAILURE! -- in com.jeequan.jeepay.core.ctrls.AbstractCtrlSortFieldTest
      [ERROR]   AbstractCtrlSortFieldTest.rejectsSortFieldThatIsNotPlainSqlIdentifier
      org.opentest4j.AssertionFailedError: 非法的排序字段必须被拒绝: role name ==> Expected com.jeequan.jeepay.core.exception.BizException to be thrown, but nothing was thrown.
      
      $ mvn -pl jeepay-manager -am test -Dtest=SysRoleControllerSortFieldTest
      [ERROR] Tests run: 4, Failures: 2, Errors: 0, Skipped: 0 <<< FAILURE! -- in com.jeequan.jeepay.mgr.ctrl.sysuser.SysRoleControllerSortFieldTest
      [ERROR]   SysRoleControllerSortFieldTest.rejectsSortFieldCarryingSqlMetaCharacters
      org.opentest4j.AssertionFailedError: Expected com.jeequan.jeepay.core.exception.BizException to be thrown, but nothing was thrown.
      [ERROR]   SysRoleControllerSortFieldTest.fallsBackToDefaultOrderWhenSortFieldIsNotAllowlisted
      org.opentest4j.AssertionFailedError:
      expected: "ORDER BY updated_at DESC"
       but was: "ORDER BY sys_type ASC"
      
      最后一条直观显示了问题:未校验时,调用方指定的列 sys_type 被原样拼进了 ORDER BY。
    • 修复后(同一命令):
      [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 -- in com.jeequan.jeepay.core.ctrls.AbstractCtrlSortFieldTest
      [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 -- in com.jeequan.jeepay.mgr.ctrl.sysuser.SysRoleControllerSortFieldTest
      [INFO] BUILD SUCCESS
      
  • 未验证项、已知风险和原因(不要把离线检查写成真实支付验收):
    • 未做真实数据库的端到端联调(本地无数据库),排序行为的判定依据是 MyBatis-Plus 实际生成的 ORDER BY 片段断言,而非页面人工观察。
    • 未在 JDK 17 下本地执行(本机为 JDK 21),仓库 CI 会以 JDK 17 重新执行 mvn -B test。

合并前核对

  • 普通贡献目标为 dev;稳定热修复到 master 并关联回同步 dev 的计划 / PR
  • 范围、验收标准和相关文档已对齐;不兼容变化已明确标注
  • 相关测试通过,未提交凭据、真实商户数据或敏感日志
  • 维护者评审意见已处理,必需 CI 检查通过
  • 若为发布 PR,已列出前后端版本、升级 / 回滚步骤及联调结果

Closes #118

Prepared with an AI coding agent.

rwzdk added 2 commits October 27, 2026 00:51
排序字段sortField来自前端请求参数,AbstractCtrl#getSortInfo 仅做了驼峰转下划线的
格式转换后便交给MyBatis-Plus拼接进ORDER BY语句,没有做任何字符或字段校验,
角色列表接口GET /api/sysRoles因此可以把任意SQL片段带入排序子句。

处理方式:
- AbstractCtrl#getSortInfo 只放行标准SQL标识符(字母开头的字母数字下划线组合),
  其余取值直接按参数错误拒绝,避免排序字段被当作SQL片段执行;
- 角色列表改为按角色表字段白名单排序,白名单外的排序字段回退到默认的更新时间倒序;
- 排序方向改用orderByAsc/orderByDesc显式表达,不再依赖位置参数含义。

Closes jeequan#118
SysRoleControllerSortFieldTest 中仅使用了 Wrapper 接口,
QueryWrapper 的导入未被使用,移除以保持编译告警干净。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant