# Multi Tenant Safety

当代码涉及多租户隔离（TenantContext、tenantId、租户拦截器/过滤器、X-Tenant-Code）时触发。防止租户越权访问、数据串租户等安全问题。

- Repository: https://github.com/doccker/cc-use-exp (1063 stars)
- Category: Engineering
- License: see repository
- Last updated: 2026-08-26
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add doccker/cc-use-exp --skill multi-tenant-safety -g -a claude-code -y`
Codex: `npx skills add doccker/cc-use-exp --skill multi-tenant-safety -g -a codex -y`
Cursor: `npx skills add doccker/cc-use-exp --skill multi-tenant-safety -g -a cursor -y`
Gemini CLI: `npx skills add doccker/cc-use-exp --skill multi-tenant-safety -g -a gemini-cli -y`
Claude Code plugin marketplace: `/plugin marketplace add doccker/cc-use-exp`

## Description

当代码涉及多租户隔离（TenantContext、tenantId、租户拦截器/过滤器、X-Tenant-Code）时触发。防止租户越权访问、数据串租户等安全问题。

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# 多租户隔离安全规范

当系统涉及多租户架构时，防止租户间数据越权访问。

---

## 陷阱 #1: 租户上下文来源信任错误

**场景**: 拦截器/过滤器从请求头（如 `X-Tenant-Code`）设置租户上下文，但未与认证 token 中的 tenantId 做一致性校验

### 问题根因

请求头可以被客户端任意伪造。如果后端只信任请求头中的租户标识，攻击者只需修改 header 就能访问其他租户的数据。

### 错误示例

```java
// ❌ 错误: 只信任请求头，未校验 token
@Override
public boolean preHandle(HttpServletRequest request, ...) {
    String tenantCode = request.getHeader("X-Tenant-Code");
    TenantMiniAppConfig config = configRepository.findByTenantCode(tenantCode);
    TenantContext.setTenantId(config.getTenantId());  // 直接信任 header
    return true;
}
// 攻击者拿着 tenantId=1 的 token，配上 X-Tenant-Code: OTHER_TENANT
// 就能读到其他租户的数据
```

### 正确做法

```java
// ✅ 正确: header 只做路由定位，必须与 token tenantId 校验一致
@Override
public boolean preHandle(HttpServletRequest request, ...) {
    String tenantCode = request.getHeader("X-Tenant-Code");
    TenantMiniAppConfig config = configRepository.findByTenantCode(tenantCode);

    // 从认证 token 中取出 tenantId（真相源）
    Long tokenTenantId = (Long) request.getAttribute("tokenTenantId");
    if (tokenTenantId != null && !tokenTenantId.equals(config.getTenantId())) {
        response.setStatus(403);
        response.getWriter().write("{\"code\":403,\"message\":\"租户信息不匹配\"}");
        return false;
    }

    TenantContext.setTenantId(config.getTenantId());
    return true;
}
```

### 检查清单

- [ ] 租户上下文的最终来源是否以认证 token 为准
- [ ] 请求头中的租户标识是否只用于路由定位，而非直接信任
- [ ] token 中的 tenantId 与请求头租户是否做了一致性校验
- [ ] 校验不通过时是否返回 403 而非静默放行

---

## 陷阱 #1.5: Service 层调用 `Repository.findById(id)` 缺租户过滤

> 与陷阱 #2 并行：陷阱 #2 是「全局过滤兜底」，本陷阱是「显式深度防御」。即使项目已用 Hibernate `@Filter` / MyBatis 拦截器，仍建议 Service 层显式调用 `findByTenantIdAndId`, , 主键直查在二级缓存命中、`getReferenceById` 等路径上常常绕过全局过滤。两层一起用，可读性也更好（看 Service 代码就知道隔离了 tenantId）。

**场景**: 即使有了全局过滤机制，开发者在 Service 里直接 `repo.findById(id)` 仍可能绕过过滤, , 主键查询常常被 JPA/Hibernate 当成"按 ID 直查"，跳过 entity filter

### 问题根因

The rest: https://github.com/doccker/cc-use-exp/blob/HEAD/.antigravity/skills/multi-tenant-safety/SKILL.md
