fundplatform/doc/security-fixes.md
zhangjf 455a20c1df 完善项目配置和测试用例
新增内容:
1. 添加 AGENTS.md 和 CLAUDE.md AI 助手配置文件
2. 添加安全修复说明文档 (doc/security-fixes.md)
3. 新增单元测试用例:
   - fund-common: TenantContextHolderTest, UserContextHolderTest, PageResultTest, ResultTest
   - fund-sys: AuthServiceImplTest, RoleServiceImplTest, TenantServiceImplTest

修改内容:
1. 数据库初始化脚本更新 (fund_sys_init.sql)
2. 前端依赖更新 (package.json)
3. 登录和密码管理功能优化:
   - 管理后台和移动端登录页面
   - 密码修改功能
4. 租户上下文处理优化 (TenantLineHandlerImpl)
5. 网关过滤器增强:
   - TenantGatewayFilter 租户过滤
   - TokenAuthFilter 认证过滤
6. Controller 层代码优化
7. DTO 和 Service 层代码改进

技术改进:
- 密码加密方式从 BCrypt 改为 MD5(前后端一致)
- 登录验证流程优化,支持多租户
- 增加日志输出便于调试
- 代码规范性和可维护性提升
2026-03-01 19:06:42 +08:00

6.9 KiB
Raw Permalink Blame History

租户隔离安全漏洞修复记录

修复日期: 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 中已认证的值:

// 安全修复:先移除客户端传来的 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_usersys_rolesys_deptsys_config 等业务表,这些表均有 tenant_id 字段属于租户私有数据。将其加入忽略列表后MyBatis-Plus 不会为这些表的查询自动注入 tenant_id 过滤条件,导致以下问题:

  1. 跨租户用户数据泄漏:查询 sys_user 时返回所有租户的用户数据
  2. 跨租户角色泄漏:查询 sys_role 时返回所有租户的角色配置
  3. 跨租户部门泄漏:查询 sys_dept 时返回所有租户的部门结构

修复方案

IGNORE_TABLES 中仅保留真正全平台共享的静态数据表:

// 修复后:仅保留平台级全局共享表
private static final Set<String> 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

// 漏洞代码
if (tenantId == null) {
    tenantId = 1L;  // 危险误操作租户1的数据
}

这导致在没有认证上下文的场景如定时任务、内部错误等SQL 会意外查询租户1的数据可能造成租户1数据泄漏或误写入。

修复方案

当租户上下文缺失时直接抛出异常,中断 SQL 执行:

// 修复后:缺失租户上下文时强制报错
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 中(全局查询),登录时仅按用户名查询不区分租户:

// 漏洞代码:不同租户可能有同名用户,查到哪个是不确定的
wrapper.eq(SysUser::getUsername, request.getUsername());
// 未加 tenantId 条件
SysUser user = userDataService.getOne(wrapper);

当不同租户有同名用户时,认证结果不确定,存在以下风险:

  • 用户 A租户1可能以租户2用户的身份登录
  • 登录成功后 Token 中绑定了错误的 tenantId

修复方案

  1. LoginRequestDTO 增加 tenantId 字段(必填),前端登录时显式指定租户
  2. 登录查询使用 TenantIgnoreHelper 跳过自动租户过滤,同时显式加入 tenant_id 条件:
// 修复后:使用 TenantIgnoreHelper + 显式 tenantId 条件
SysUser user = TenantIgnoreHelper.ignore(() -> {
    LambdaQueryWrapper<SysUser> 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 中清理