9.8 KiB
9.8 KiB
安全漏洞: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 漏洞原理
# 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 体系(推荐)
# 我们的 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 生成服务
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 激活码生成服务
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. 数据库设计
-- 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 识别逻辑
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 缓解措施
-
强制 Key 来源验证
- 所有 Key 必须包含平台标识
- 验证时必须查询数据库
-
Key 轮换
- 定期轮换 Key
- 用户可手动轮换
-
使用监控
- 记录 Key 使用情况
- 异常使用告警
-
IP 限制
- 支持 IP 白名单
- 异常 IP 告警
8. 结论
-
Subapi 存在严重安全漏洞:API Key 不验证来源,可在任意部署使用
-
我们的系统必须自建 Key 体系:
- Key 必须包含平台标识
- 必须数据库验证
- 必须防伪造
-
集成时流量分离:
- 我们的 Key 由我们处理
- Subapi Key 转发到 Subapi
文档状态:安全漏洞分析 关联文档:
supply_detailed_design_v1_2026-03-18.md