# 租户隔离安全漏洞修复记录 **修复日期**: 2026-03-01 **修复分支**: master **修复范围**: 多租户数据隔离机制 --- ## 漏洞一:网关未覆盖客户端伪造的 X-Tenant-Id(高危) ### 漏洞描述 `TokenAuthFilter` 在 Token 验证通过后,使用 Spring WebFlux 的 `request.mutate().header()` 将 Token 中的 tenantId 写入请求头。然而 WebFlux 的 `header()` 方法是**追加**而非替换语义,若客户端预先在请求中设置了 `X-Tenant-Id`,下游服务从 `getFirst()` 取到的仍然是客户端伪造的值。攻击者可以通过在请求中携带任意 `X-Tenant-Id` 来访问其他租户的数据。 ### 攻击场景 ``` 攻击者发送请求: Authorization: Bearer <合法Token,属于租户A> X-Tenant-Id: 999 ← 伪造的租户ID ``` 原始代码中,下游 `ContextInterceptor` 从请求头取到的 `X-Tenant-Id` 可能是 999,导致 `TenantContextHolder` 被设置为 999,进而 MyBatis-Plus 租户插件以 tenant_id=999 过滤 SQL,越权访问其他租户数据。 ### 修复方案 在 `TokenAuthFilter` 中先移除客户端传来的 `X-Tenant-Id`,再写入 Token 中已认证的值: ```java // 安全修复:先移除客户端传来的 X-Tenant-Id,再写入 Token 中已认证的值 ServerHttpRequest mutatedRequest = request.mutate() .headers(headers -> headers.remove(TENANT_ID_HEADER)) // 先删除客户端值 .header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId())) .header(USERNAME_HEADER, tokenInfo.getUsername()) .header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId())) // 再写入认证值 .build(); ``` ### 修复位置 `fund-gateway/src/main/java/com/fundplatform/gateway/filter/TokenAuthFilter.java` 第 86-93 行 --- ## 漏洞二:IGNORE_TABLES 包含业务敏感表(高危) ### 漏洞描述 `TenantLineHandlerImpl.IGNORE_TABLES` 中包含了 `sys_user`、`sys_role`、`sys_dept`、`sys_config` 等业务表,这些表均有 `tenant_id` 字段,属于租户私有数据。将其加入忽略列表后,MyBatis-Plus 不会为这些表的查询自动注入 `tenant_id` 过滤条件,导致以下问题: 1. **跨租户用户数据泄漏**:查询 `sys_user` 时返回所有租户的用户数据 2. **跨租户角色泄漏**:查询 `sys_role` 时返回所有租户的角色配置 3. **跨租户部门泄漏**:查询 `sys_dept` 时返回所有租户的部门结构 ### 修复方案 `IGNORE_TABLES` 中仅保留真正全平台共享的静态数据表: ```java // 修复后:仅保留平台级全局共享表 private static final Set IGNORE_TABLES = new HashSet<>(Arrays.asList( "sys_menu", // 菜单表(系统菜单结构全局共享) "sys_dict", // 字典表(枚举数据全局共享) "sys_log", // 日志表(独立存储逻辑) "gen_table", // 代码生成表 "gen_table_column" )); // 已移除:sys_user、sys_role、sys_dept、sys_config(均为租户私有数据) ``` ### 修复位置 `fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 34-46 行 --- ## 漏洞三:租户 ID 缺失时 Fallback 到默认值 1(中危) ### 漏洞描述 `TenantLineHandlerImpl.getTenantId()` 中,当从 `TenantContextHolder` 获取不到租户 ID 时,代码回退到默认值 `1L`: ```java // 漏洞代码 if (tenantId == null) { tenantId = 1L; // 危险:误操作租户1的数据 } ``` 这导致在没有认证上下文的场景(如定时任务、内部错误等)下,SQL 会意外查询租户1的数据,可能造成租户1数据泄漏或误写入。 ### 修复方案 当租户上下文缺失时直接抛出异常,中断 SQL 执行: ```java // 修复后:缺失租户上下文时强制报错 if (tenantId == null) { throw new IllegalStateException("[Security] 当前请求缺少租户上下文,拒绝执行SQL"); } ``` ### 修复位置 `fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 56-66 行 --- ## 漏洞四:登录接口跨租户用户名混乱(中危) ### 漏洞描述 登录接口 `/auth/login` 在 Token 白名单中,`TenantContextHolder` 为空。由于 `sys_user` 原来在 `IGNORE_TABLES` 中(全局查询),登录时仅按用户名查询不区分租户: ```java // 漏洞代码:不同租户可能有同名用户,查到哪个是不确定的 wrapper.eq(SysUser::getUsername, request.getUsername()); // 未加 tenantId 条件 SysUser user = userDataService.getOne(wrapper); ``` 当不同租户有同名用户时,认证结果不确定,存在以下风险: - 用户 A(租户1)可能以租户2用户的身份登录 - 登录成功后 Token 中绑定了错误的 tenantId ### 修复方案 1. `LoginRequestDTO` 增加 `tenantId` 字段(必填),前端登录时显式指定租户 2. 登录查询使用 `TenantIgnoreHelper` 跳过自动租户过滤,同时显式加入 `tenant_id` 条件: ```java // 修复后:使用 TenantIgnoreHelper + 显式 tenantId 条件 SysUser user = TenantIgnoreHelper.ignore(() -> { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUsername, request.getUsername()); wrapper.eq(SysUser::getTenantId, request.getTenantId()); // 显式限定租户 wrapper.eq(SysUser::getDeleted, 0); return userDataService.getOne(wrapper); }); ``` ### 修复位置 - `fund-sys/src/main/java/com/fundplatform/sys/dto/LoginRequestDTO.java` — 新增 tenantId 字段 - `fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java` 第 31-44 行 --- ## 修复总结 | 编号 | 漏洞类型 | 危险级别 | 修复文件 | |------|---------|---------|---------| | 1 | 网关未覆盖客户端伪造的 X-Tenant-Id | 高危 | `fund-gateway/.../TokenAuthFilter.java` | | 2 | IGNORE_TABLES 包含业务敏感表 | 高危 | `fund-common/.../TenantLineHandlerImpl.java` | | 3 | 租户 ID 缺失时 fallback 到默认值 | 中危 | `fund-common/.../TenantLineHandlerImpl.java` | | 4 | 登录查询未限定租户范围 | 中危 | `fund-sys/.../AuthServiceImpl.java`, `LoginRequestDTO.java` | ## 修复后的安全保证 1. **网关层**:经过 Token 验证的请求,`X-Tenant-Id` 由 Token 中的认证值强制覆盖,客户端无法伪造 2. **数据层**:业务表(sys_user、sys_role、sys_dept、sys_config)均受 MyBatis-Plus 租户插件保护,自动注入 `tenant_id` 过滤 3. **兜底保护**:租户上下文为空时 SQL 拒绝执行,不会 fallback 到任何租户 4. **登录安全**:登录时必须指定 tenantId,确保同名用户不会跨租户混乱认证 ## 注意事项 - 所有需要跨租户操作的合法场景(如超管管理所有租户),必须显式使用 `TenantIgnoreHelper.ignore()` 包装,并在代码中注明安全意图 - 定时任务、事件监听器等异步场景在执行 DB 操作前,必须先设置 `TenantContextHolder`,执行完后在 finally 中清理