[Security] JWT userId 未与 Redis 在线会话绑定,导致跨用户身份混淆与条件性垂直越权
[Security] JWT userId 未与 Redis 在线会话绑定,导致跨用户身份混淆与条件性垂直越权
您好,我们在最新版 master 分支上独立复现了一个与密码重置问题不同的 JWT 身份绑定缺陷。
该问题不是 #902 / #910 所报告的“低权限用户重置任意用户密码”问题,而是:应用使用 Redis 验证 sub + uid 对应的在线会话是否存在,但后续业务逻辑直接信任 JWT 中可被重新签名修改的 userId。因此,攻击者可以保留自己合法会话的 sub 和 uid,仅修改 userId,使不同安全边界使用不同的用户身份。
测试版本
- Repository: https://github.com/elunez/eladmin
- Branch:
master - Tested commit:
55fbf705956949697dbd68bf9003776609d3d029 - Tested date: 2026-08-04
- 测试使用本地隔离部署,未修改应用源代码。
根因
1. JWT 签名密钥公开且固定
application-dev.yml 和 application-prod.yml 中都包含固定的 Base64 JWT 密钥,密钥随公开源代码可获得:
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/resources/config/application-dev.yml#L85
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/resources/config/application-prod.yml#L89
TokenProvider 使用该密钥创建 HS512 签名:
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java#L57-L65
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java#L75-L90
2. Redis 校验没有绑定用户 ID
Token 的 Redis key 由 JWT 的 sub 和 uid 组成:
过滤器只检查这个 Redis 在线记录是否存在,然后建立认证上下文;没有比较 JWT 的 userId 与 Redis 中服务端在线用户对应的真实用户 ID:
3. 业务逻辑直接信任 JWT userId
SecurityUtils.getCurrentUserId() 直接从 JWT payload 读取 userId:
例如,个人资料接口使用这个值判断请求者是否正在修改自己的资料:
用户角色级别检查也使用这个值:
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java#L120-L125
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java#L199-L209
- https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java#L139-L152
菜单构建接口同样使用 JWT 中的 userId 查询菜单:
复现条件
攻击者需要:
- 拥有一个普通的有效账户,能够通过正常登录获得在线会话;
- 能够读取公开源代码中的 JWT 密钥;
- 使用该密钥重新签发一个 JWT,保留自己合法会话的
sub和uid,将userId修改为目标账户 ID。
伪造后的 payload 形式如下,其中 uid 必须是攻击者自己正常登录获得的值:
{
"sub": "test",
"uid": "<legitimate-test-uid>",
"userId": 1
}
由于 Redis key 仍然是合法用户对应的:
online_token:test:<legitimate-test-uid>
过滤器接受该 Token,但业务层读取到的当前用户 ID 已经是 1。
动态验证结果
Redis 能阻止直接伪造管理员 subject
我们首先验证了该问题不是简单的“普通用户把 sub 改成 admin 即可直接登录管理员账号”:
随机 uid + sub=admin -> HTTP 401
合法 test uid + sub=admin -> HTTP 401
合法 test uid + sub=test + userId=1 -> 请求被接受
这说明 Redis 的 sub + uid 检查仍然存在,但它没有保护 userId 这一单独的身份来源。
默认权限配置下的结果
在默认本地部署中,使用普通 test 用户的合法 Token 与伪造 Token 对比:
| 接口 | 合法 test Token | 伪造 userId=1 Token |
|---|---|---|
GET /api/roles/level |
返回普通用户角色级别 2 |
返回管理员对应级别 1 |
GET /api/menus/build |
返回普通用户菜单树 | 返回管理员菜单树 |
PUT /api/users/center,请求体 ID 为 1 |
HTTP 400 | HTTP 204 |
使用伪造 Token 调用个人资料接口后,管理员账户的 nickName 和 phone 被修改。该接口本身只更新个人资料字段,并没有证明可以通过该接口直接修改管理员密码或角色。
具有用户管理权限时的条件性垂直越权
默认 test 账户没有 user:edit 权限,因此调用 PUT /api/users 会先被权限注解拦截。为了验证后续角色级别检查,我们只在隔离测试数据库中临时授予 test user:edit 权限:
合法 test Token + PUT /api/users -> 角色级别检查拒绝
伪造 userId=1 Token + PUT /api/users -> HTTP 204
伪造 Token -> 管理员邮箱被修改
这是因为 checkLevel() 使用 SecurityUtils.getCurrentUserId() 查询当前用户角色级别。伪造 userId=1 后,服务端将管理员的角色级别作为“当前操作者”级别进行比较,导致该检查失效。
影响范围
默认部署可确认的影响
- 修改其他用户的昵称、电话等个人资料;
- 获取管理员菜单树及用户角色级别等身份相关信息;
- 造成请求者身份与服务端在线会话身份不一致。
特定角色配置下的影响
如果部署环境向非管理员角色授予 user:edit,同样的身份混淆会影响用户修改接口的角色级别检查,可能进一步修改管理员账户字段。user:add 和 user:del 也包含基于当前用户 ID 的角色级别逻辑,建议一并修复并加入回归测试;本次动态验证重点确认了 user:edit 路径。
因此,该问题不是“任何普通用户无条件直接变成管理员”,而是一个由固定 JWT 密钥和缺失身份绑定共同造成的跨用户授权绕过。实际严重程度取决于部署环境为低权限角色授予的权限。
与已有密码重置问题的区别
本 Issue 不重复报告以下已有问题:
上述问题关注的是未授权用户通过 PUT /api/users/resetPwd 重置任意用户密码。本 Issue 关注的是 JWT userId 与 Redis 在线会话身份没有绑定,影响个人资料、菜单/角色上下文和特定权限配置下的用户管理授权检查。两者根因和利用路径不同。
修复建议
立即轮换已经公开的 JWT 密钥,不要再将生产密钥放在仓库配置文件中;
在服务端在线用户记录中保存真实用户 ID,并验证:
JWT.sub == server-side username JWT.uid == server-side session uid JWT.userId == server-side user id更推荐从已经验证的
sub和服务端在线用户记录中解析当前用户 ID,不要将 JWTuserId作为业务授权的可信来源;修复
centerUser()、checkLevel()、用户删除和菜单构建等调用点,统一使用服务端解析出的当前用户身份;为伪造
userId的 Token 增加回归测试,并覆盖user:add、user:edit、user:del等委派权限场景。
感谢维护该项目。我们愿意提供更完整的复现日志、测试脚本,并在维护者确认修复方向后协助提交 Pull Request。
Source: elunez/eladmin