JeecgBoot Tenant Privilege Escalation: GET /sys/sysDepartPermission/list Department Permission List Query Without Tenant Validation
Contributors: huangweigang
1. Impact Scope
2. Vulnerable Endpoint
- GET
/sys/sysDepartPermission/list?departId=...&pageNo=1&pageSize=10 (Department Permission Table Paginated List Query API)
3. Code Analysis
- Controller:
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartPermissionController.java
- Route & method:
@GetMapping(value = "/list")
public Result<?> queryPageList(SysDepartPermission sysDepartPermission, @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req)
- Key code (lines 75–84):
QueryWrapper<SysDepartPermission> queryWrapper = QueryGenerator.initQueryWrapper(sysDepartPermission, req.getParameterMap());
Page<SysDepartPermission> page = new Page<SysDepartPermission>(pageNo, pageSize);
IPage<SysDepartPermission> pageList = sysDepartPermissionService.page(page, queryWrapper);
return Result.ok(pageList);
- Problem points:
- The endpoint uses
QueryGenerator.initQueryWrapper to automatically build query conditions, supporting filtering via departId parameter
- Does not validate tenant ownership of the
departId parameter
- Attackers can query other tenants' department permission configurations by specifying arbitrary department IDs
- Completely relies on frontend-provided query parameters, lacking server-side permission verification
4. Reproduction
-- Prerequisites
- Attacker has a valid login session
- Attacker knows or can enumerate the target tenant's department ID
- System has department-level permission authorization configured
-- Steps (Cross-tenant Department Permission Configuration Disclosure)
- Using attacker account (Tenant A):
curl -X GET -H "Authorization: Bearer <attacker_token>" "http://<host>/jeecgboot/sys/sysDepartPermission/list?departId=<victim_dept_id>&pageNo=1&pageSize=100"
- Observation: API returns 200 OK, returns the target department's permission configuration list, including:
- Department ID (departId)
- Permission ID (permissionId)
- Data Rule IDs (dataRuleIds)
- Metadata such as creation time
- Verification:
- Use Tenant B's administrator account to query the same department's permission configuration and confirm data consistency
- Database query sys_depart_permission table to confirm data ownership
5. Impact
- Cross-tenant permission configuration disclosure
- Attackers can obtain any tenant's department permission configuration information
- Understand which menus and functions are authorized to which departments
- Organizational structure and permission system exposure
- Permission configuration can be reverse-engineered to infer the organization's department structure and functional divisions
- Business functions and modules disclosure
- Permission IDs correspond to functional modules, exposing the system's business functional architecture
- Provides foundation for privilege escalation attacks
- After understanding permission configuration, attackers can specifically search for privilege escalation vulnerabilities
6. Remediation
- Department tenant ownership validation
- When accepting
departId parameter, verify whether the department belongs to the current tenant:
if(oConvertUtils.isNotEmpty(sysDepartPermission.getDepartId())) {
SysDepart depart = sysDepartService.getById(sysDepartPermission.getDepartId());
LoginUser currentUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if(depart == null || !depart.getTenantId().equals(currentUser.getTenantId())) {
return Result.error("Unauthorized to access this department permission data");
}
}
- Force tenant filtering
- Forcibly add tenant ID filtering condition in QueryWrapper:
queryWrapper.eq("tenant_id", TenantContext.getTenant());
- Database-level tenant isolation
- Configure MyBatis-Plus multi-tenancy plugin for automatic filtering at SQL level
- Parameter whitelist
- Restrict allowed query parameter fields to prevent cross-tenant queries via arbitrary fields
- Audit logging
- Record all department permission query operations, especially focus on cross-tenant access attempts
JeecgBoot Tenant Privilege Escalation: GET /sys/sysDepartPermission/list Department Permission List Query Without Tenant Validation
Contributors: huangweigang
1. Impact Scope
2. Vulnerable Endpoint
/sys/sysDepartPermission/list?departId=...&pageNo=1&pageSize=10(Department Permission Table Paginated List Query API)3. Code Analysis
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDepartPermissionController.java@GetMapping(value = "/list")public Result<?> queryPageList(SysDepartPermission sysDepartPermission, @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req)QueryWrapper<SysDepartPermission> queryWrapper = QueryGenerator.initQueryWrapper(sysDepartPermission, req.getParameterMap());Page<SysDepartPermission> page = new Page<SysDepartPermission>(pageNo, pageSize);IPage<SysDepartPermission> pageList = sysDepartPermissionService.page(page, queryWrapper);return Result.ok(pageList);QueryGenerator.initQueryWrapperto automatically build query conditions, supporting filtering viadepartIdparameterdepartIdparameter4. Reproduction
-- Prerequisites
-- Steps (Cross-tenant Department Permission Configuration Disclosure)
curl -X GET -H "Authorization: Bearer <attacker_token>" "http://<host>/jeecgboot/sys/sysDepartPermission/list?departId=<victim_dept_id>&pageNo=1&pageSize=100"5. Impact
6. Remediation
departIdparameter, verify whether the department belongs to the current tenant:if(oConvertUtils.isNotEmpty(sysDepartPermission.getDepartId())) {SysDepart depart = sysDepartService.getById(sysDepartPermission.getDepartId());LoginUser currentUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();if(depart == null || !depart.getTenantId().equals(currentUser.getTenantId())) {return Result.error("Unauthorized to access this department permission data");}}queryWrapper.eq("tenant_id", TenantContext.getTenant());