chore: initial public snapshot for github upload
This commit is contained in:
399
docs/security_api_key_vulnerability_analysis_v1_2026-03-18.md
Normal file
399
docs/security_api_key_vulnerability_analysis_v1_2026-03-18.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# 安全漏洞: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`
|
||||
Reference in New Issue
Block a user