# 安全漏洞：Subapi API Key 跨部署验证问题

> 发现时间：2026-03-18
> 漏洞等级：**严重（P0）**
> 状态：历史漏洞分析文档（用于复盘），不作为当前实现基线。
> 实施基线：`security_solution_v1_2026-03-18.md`（HMAC-SHA256 方案）。

---

## 1. 漏洞描述

### 1.1 问题现象

Subapi 分发给用户的 API Key 和激活码只验证算法正确性，未验证 Key 是否由当前系统生成。

这意味着：
- 部署在 A 服务器的 Subapi 生成的 API Key，可以在部署在 B 服务器的 Subapi 中通过验证
- 不同独立部署之间的 API Key 可以互相串用

### 1.2 漏洞原理

```python
# Subapi 当前的验证逻辑（推测）
def verify_api_key(key):
    # 只验证格式和算法
    if validate_format(key) and validate_checksum(key):
        return True  # 通过验证
    return False

# 问题：没有验证 Key 的来源（哪个部署生成的）
```

### 1.3 影响范围

| 场景 | 影响 |
|------|------|
| 平台间串用 | 用户的 Key 可能在其他平台也能用 |
| 账号盗用 | 窃取的 Key 可以在任意部署使用 |
| 收益损失 | 供应方的配额可能被其他平台盗用 |
| 账务错误 | 调用记录和计费可能记到错误平台 |

---

## 2. 漏洞影响我们的规划

### 2.1 如果集成 Subapi

- 我们的用户可能使用其他 Subapi 部署生成的 Key
- 我们的计费可能被绕过
- 供应方的收益可能被截取

### 2.2 解决方案

**方案 A：自建 API Key 体系（推荐）**

```python
# 我们的 API Key 设计
def generate_api_key(user_id, platform_id):
    # Key 结构：{platform_prefix}{version}{user_hash}{checksum}
    # platform_prefix: 我们的平台标识（如 "LGW"）
    # user_hash: 用户ID的哈希
    # checksum: CRC32/MD5 校验

    key = f"lgw_{version}_{user_hash}_{checksum}"
    return key

def verify_api_key(key):
    # 1. 验证格式
    # 2. 验证平台标识（我们的平台）
    # 3. 验证校验和
    # 4. 验证是否在我们的数据库中

    if not key.startswith("lgw_"):
        return False  # 不是我们的 Key

    # 继续验证...
    return True
```

**方案 B：使用 Token 代替 API Key**

- 不直接传递 API Key
- 使用 OAuth 2.0 风格的 Access Token
- Token 绑定到具体部署，无法跨部署使用

---

## 3. 我们的 API Key 设计规范

### 3.1 Key 结构

```
{LGW}-{版本}-{用户哈希}-{时间戳}-{随机数}-{校验和}

示例：
lgw-v1-u7f3a2b1-t1700000000-r8f3a2-e9d4c1b2
```

| 字段 | 说明 | 长度 |
|------|------|------|
| LGW | 平台标识 | 3 |
| v1 | 版本号 | 2 |
| u7f3a2b1 | 用户哈希 | 8 |
| t1700000000 | 时间戳 | 10 |
| r8f3a2 | 随机数 | 6 |
| e9d4c1b2 | 校验和 | 8 |

### 3.2 验证流程

```
收到 API Key 请求
    │
    ▼
┌─────────────────┐
│ 1. 格式验证    │ ──▶ 格式错误 → 400
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 2. 平台标识    │ ──▶ 不是 "lgw-" → 401
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 3. 校验和验证  │ ──▶ 校验失败 → 401
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 4. 数据库验证   │ ──▶ Key 不存在/已禁用 → 401
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 5. 权限验证    │ ──▶ 无权限 → 403
└────────┬────────┘
         │
         ▼
    验证通过
```

### 3.3 激活码设计

```
{LGW}-{类型}-{用户ID}-{过期时间}-{随机数}-{校验和}

示例：
lgw-act-1000-20260331-r8f3a2-e9d4c1b2
```

| 字段 | 说明 |
|------|------|
| lgw | 平台标识 |
| act | 激活码类型 |
| 1000 | 用户ID |
| 20260331 | 过期日期 |
| r8f3a2 | 随机数 |
| e9d4c1b2 | 校验和 |

---

## 4. 技术实现

### 4.1 Key 生成服务

```python
import hashlib
import secrets
import time

class APIKeyGenerator:
    PLATFORM_PREFIX = "lgw"
    VERSION = "v1"

    @classmethod
    def generate(cls, user_id: int) -> str:
        # 用户哈希（8位）
        user_hash = hashlib.md5(str(user_id).encode()).hexdigest()[:8]

        # 时间戳（10位）
        timestamp = str(int(time.time()))

        # 随机数（6位）
        random = secrets.token_hex(3)[:6]

        # 组合
        raw = f"{cls.PLATFORM_PREFIX}-{cls.VERSION}-{user_hash}-{timestamp}-{random}"

        # 校验和（8位）
        checksum = hashlib.md5(raw.encode()).hexdigest()[:8]

        return f"{raw}-{checksum}"

    @classmethod
    def verify(cls, key: str) -> bool:
        # 1. 格式验证
        parts = key.split("-")
        if len(parts) != 6:
            return False

        # 2. 平台标识验证
        if parts[0] != cls.PLATFORM_PREFIX:
            return False

        # 3. 校验和验证
        raw = "-".join(parts[:5])
        expected_checksum = hashlib.md5(raw.encode()).hexdigest()[:8]
        if parts[5] != expected_checksum:
            return False

        # 4. 数据库验证（在 Controller 中实现）
        return True
```

### 4.2 激活码生成服务

```python
class ActivationCodeGenerator:
    PLATFORM_PREFIX = "lgw"
    CODE_TYPE = "act"

    @classmethod
    def generate(cls, user_id: int, expiry_days: int) -> str:
        # 计算过期日期
        expiry = datetime.now() + timedelta(days=expiry_days)
        expiry_str = expiry.strftime("%Y%m%d")

        # 随机数
        random = secrets.token_hex(3)[:6]

        # 组合
        raw = f"{cls.PLATFORM_PREFIX}-{cls.CODE_TYPE}-{user_id}-{expiry_str}-{random}"

        # 校验和
        checksum = hashlib.md5(raw.encode()).hexdigest()[:8]

        return f"{raw}-{checksum}"
```

---

## 5. 数据库设计

```sql
-- API Keys 表
CREATE TABLE api_keys (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    key_hash VARCHAR(64) NOT NULL UNIQUE COMMENT 'Key 的哈希（用于查询）',
    key_prefix VARCHAR(20) NOT NULL COMMENT 'Key 前缀（用于展示）',

    -- 绑定信息
    team_id BIGINT,
    organization_id BIGINT,

    -- 权限
    permissions JSON COMMENT '权限列表',
    allowed_models JSON COMMENT '允许的模型列表',
    allowed_ips JSON COMMENT 'IP 白名单',

    -- 限制
    rate_limit_rpm INT DEFAULT 60,
    rate_limit_tpm INT DEFAULT 100000,
    max_concurrent INT DEFAULT 10,

    -- 状态
    status VARCHAR(20) DEFAULT 'active' COMMENT 'active/disabled/expired',

    -- 时间
    expires_at TIMESTAMP,
    last_used_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    -- 审计
    created_by BIGINT,
    ip_address VARCHAR(45),
    description VARCHAR(200),

    INDEX idx_user_id (user_id),
    INDEX idx_key_hash (key_hash),
    INDEX idx_status (status),
    INDEX idx_expires_at (expires_at)
) COMMENT 'API Keys 表';

-- 激活码表
CREATE TABLE activation_codes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    code_hash VARCHAR(64) NOT NULL UNIQUE COMMENT '激活码哈希',
    code_prefix VARCHAR(20) NOT NULL COMMENT '激活码前缀',

    -- 绑定信息
    user_id BIGINT NOT NULL,
    target_type VARCHAR(20) COMMENT '激活目标类型: subscription/package',
    target_id BIGINT COMMENT '激活目标ID',

    -- 状态
    status VARCHAR(20) DEFAULT 'unused' COMMENT 'unused/used/expired',
    used_at TIMESTAMP,
    used_by BIGINT COMMENT '使用者ID',

    -- 时间
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_code_hash (code_hash),
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_expires_at (expires_at)
) COMMENT '激活码表';
```

---

## 6. 与 Subapi 集成时的处理

### 6.1 方案：我们的 Gateway 作为唯一入口

```
用户请求
    │
    ▼
我们的 Gateway（验证 Key 来源）
    │
    ├── 我们的 Key → 处理
    │
    └── Subapi 格式的 Key → 拒绝或转发到 Subapi
```

### 6.2 API Key 识别逻辑

```python
def identify_key_type(key: str) -> str:
    if key.startswith("lgw-"):
        return "own"  # 我们的 Key
    elif key.startswith("sk-"):
        return "openai"  # OpenAI 原始 Key
    else:
        return "unknown"  # 未知类型
```

### 6.3 流量分离

| Key 类型 | 处理方式 |
|----------|----------|
| `lgw-` 开头 | 我们的 Gateway 处理 |
| `sk-` 开头 | 直接转发到对应供应商 |
| 其他 Subapi 格式 | 转发到 Subapi（如果有集成） |

---

## 7. 风险评估与缓解

### 7.1 风险评估

| 风险 | 影响 | 可能性 | 严重性 |
|------|------|--------|--------|
| Subapi Key 串用 | 计费损失/账号盗用 | 高 | 严重 |
| 激活码伪造 | 权益被盗用 | 中 | 高 |
| Key 泄露 | 未授权使用 | 高 | 高 |

### 7.2 缓解措施

1. **强制 Key 来源验证**
   - 所有 Key 必须包含平台标识
   - 验证时必须查询数据库

2. **Key 轮换**
   - 定期轮换 Key
   - 用户可手动轮换

3. **使用监控**
   - 记录 Key 使用情况
   - 异常使用告警

4. **IP 限制**
   - 支持 IP 白名单
   - 异常 IP 告警

---

## 8. 结论

1. **Subapi 存在严重安全漏洞**：API Key 不验证来源，可在任意部署使用

2. **我们的系统必须自建 Key 体系**：
   - Key 必须包含平台标识
   - 必须数据库验证
   - 必须防伪造

3. **集成时流量分离**：
   - 我们的 Key 由我们处理
   - Subapi Key 转发到 Subapi

---

**文档状态**：安全漏洞分析
**关联文档**：
- `supply_detailed_design_v1_2026-03-18.md`
