- Remove old review reports (keep latest only) - Move docs/ to deploy/docs-backup/ - Move performance-testing/ to deploy/ - Clean up test output files - Organize root directory
258 lines
6.4 KiB
Markdown
258 lines
6.4 KiB
Markdown
# Sub2API 安全问题分析:激活码与API Key跨实例使用风险
|
||
|
||
## 问题描述
|
||
|
||
用户发现Sub2API系统生成的激活码(Redeem Code)和API Key在验证时未检查是否是本系统发放的,这意味着其他部署的Sub2API实例生成的激活码或API Key可能会在本系统中被接受和使用。
|
||
|
||
## 问题分析
|
||
|
||
### 1. 当前实现
|
||
|
||
#### 1.1 激活码(Redeem Code)生成
|
||
|
||
```go
|
||
// service/redeem_service.go - GenerateRandomCode
|
||
func (s *RedeemService) GenerateRandomCode() (string, error) {
|
||
// 纯随机生成,无系统标识
|
||
bytes := make([]byte, 16)
|
||
if _, err := rand.Read(bytes); err != nil {
|
||
return "", fmt.Errorf("generate random bytes: %w", err)
|
||
}
|
||
|
||
code := hex.EncodeToString(bytes)
|
||
// 格式:XXXX-XXXX-XXXX-XXXX
|
||
return formatCode(code), nil
|
||
}
|
||
```
|
||
|
||
**问题**:激活码只是32位十六进制随机字符串,没有包含任何系统标识信息。任何Sub2API实例都能生成相同格式的激活码。
|
||
|
||
#### 1.2 API Key生成
|
||
|
||
```go
|
||
// service/api_key_service.go - GenerateKey
|
||
func (s *APIKeyService) GenerateKey() (string, error) {
|
||
// 格式:sk-{32位随机字符}
|
||
bytes := make([]byte, 16)
|
||
if _, err := rand.Read(bytes); err != nil {
|
||
return "", err
|
||
}
|
||
return "sk-" + hex.EncodeToString(bytes), nil
|
||
}
|
||
```
|
||
|
||
**问题**:API Key也只是随机字符串,没有包含实例标识。
|
||
|
||
#### 1.3 验证逻辑
|
||
|
||
```go
|
||
// service/redeem_service.go - Redeem
|
||
func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (*RedeemCode, error) {
|
||
// 只检查码是否存在,不检查来源
|
||
redeemCode, err := s.redeemRepo.GetByCode(ctx, code)
|
||
if err != nil {
|
||
if errors.Is(err, ErrRedeemCodeNotFound) {
|
||
return nil, ErrRedeemCodeNotFound
|
||
}
|
||
return nil, fmt.Errorf("get redeem code: %w", err)
|
||
}
|
||
|
||
// 检查状态和有效期
|
||
if !redeemCode.CanUse() {
|
||
return nil, ErrRedeemCodeUsed
|
||
}
|
||
// ... 兑换逻辑
|
||
}
|
||
```
|
||
|
||
**验证流程**:
|
||
1. 数据库中查找激活码
|
||
2. 检查状态是否为"unused"
|
||
3. 检查是否过期
|
||
|
||
**缺陷**:没有验证激活码是否"由本系统生成"。
|
||
|
||
### 2. 影响范围
|
||
|
||
| 资源类型 | 受影响场景 | 风险等级 |
|
||
|----------|-------------|-----------|
|
||
| **激活码** | 兑换余额/并发/订阅 | 高 |
|
||
| **API Key** | API访问 | 高 |
|
||
| **管理员API Key** | 管理操作 | 极高 |
|
||
|
||
### 3. 攻击场景
|
||
|
||
#### 场景1:跨实例激活码兑换
|
||
|
||
```
|
||
攻击者:
|
||
1. 在自己的Sub2API实例A生成激活码
|
||
2. 将激活码在受害者实例B兑换
|
||
3. 如果激活码未被使用,可能成功兑换
|
||
```
|
||
|
||
#### 场景2:窃取API Key
|
||
|
||
```
|
||
攻击者:
|
||
1. 通过某种方式获取他人的API Key
|
||
2. 在自己的Sub2API实例使用该Key
|
||
3. 如果Key有效,可以访问受害者账户
|
||
```
|
||
|
||
## 解决方案
|
||
|
||
### 方案一:在Key中嵌入实例标识(推荐)
|
||
|
||
#### 实现思路
|
||
|
||
在生成激活码和API Key时,嵌入系统实例的唯一标识(如实例ID、域名等)。
|
||
|
||
#### 激活码格式改进
|
||
|
||
```go
|
||
// 建议格式:{前缀}-{实例ID}-{随机}
|
||
const CodePrefix = "S2P" // Sub2API Prefix
|
||
|
||
func (s *RedeemService) GenerateCodeWithInstanceID(instanceID string) string {
|
||
// 实例ID(8位)
|
||
instancePart := fmt.Sprintf("%-8s", instanceID[:8])
|
||
|
||
// 随机部分(24位)
|
||
randomBytes := make([]byte, 12)
|
||
rand.Read(randomBytes)
|
||
randomPart := hex.EncodeToString(randomBytes)
|
||
|
||
return fmt.Sprintf("%s-%s-%s-%s",
|
||
CodePrefix,
|
||
instancePart,
|
||
randomPart[:8],
|
||
randomPart[8:])
|
||
}
|
||
```
|
||
|
||
#### 验证逻辑
|
||
|
||
```go
|
||
func (s *RedeemService) ValidateCode(code string) error {
|
||
parts := strings.Split(code, "-")
|
||
if len(parts) != 4 {
|
||
return ErrInvalidCodeFormat
|
||
}
|
||
|
||
// 验证前缀
|
||
if parts[0] != CodePrefix {
|
||
return ErrCodePrefixMismatch
|
||
}
|
||
|
||
// 验证实例ID
|
||
instanceID := parts[1]
|
||
if instanceID != s.instanceID {
|
||
return ErrCodeFromOtherInstance
|
||
}
|
||
|
||
// 继续验证其他逻辑...
|
||
}
|
||
```
|
||
|
||
#### API Key格式改进
|
||
|
||
```go
|
||
// 建议格式:sk-{实例ID简称}-{随机}
|
||
const (
|
||
KeyPrefix = "sk"
|
||
InstanceShort = "s2p" // 简写
|
||
)
|
||
|
||
func (s *APIKeyService) GenerateKey() string {
|
||
randomBytes := make([]byte, 16)
|
||
rand.Read(randomBytes)
|
||
|
||
return fmt.Sprintf("%s-%s%s",
|
||
KeyPrefix,
|
||
InstanceShort,
|
||
hex.EncodeToString(randomBytes))
|
||
}
|
||
```
|
||
|
||
### 方案二:数据库隔离(替代方案)
|
||
|
||
#### 实现思路
|
||
|
||
在数据库中为每个实例分配唯一标识,所有生成的激活码和API Key都关联到该标识。
|
||
|
||
#### 实现
|
||
|
||
```go
|
||
// 添加实例ID字段
|
||
type RedeemCode struct {
|
||
InstanceID string // 实例标识
|
||
Code string
|
||
// ...
|
||
}
|
||
|
||
// 生成时设置实例ID
|
||
func (s *RedeemService) GenerateCode() *RedeemCode {
|
||
return &RedeemCode{
|
||
InstanceID: s.config.InstanceID,
|
||
Code: generateCode(),
|
||
}
|
||
}
|
||
|
||
// 验证时检查实例ID
|
||
func (s *RedeemService) Redeem(ctx context.Context, code string) error {
|
||
dbCode, _ := s.redeemRepo.GetByCode(ctx, code)
|
||
if dbCode.InstanceID != s.config.InstanceID {
|
||
return ErrCodeFromOtherInstance
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 实施建议
|
||
|
||
### 1. 短期措施
|
||
|
||
1. **记录实例标识**:记录每个激活码/Key的生成实例(在日志中)
|
||
2. **增强监控**:监控来自异常位置的兑换请求
|
||
3. **用户教育**:提醒用户不要泄露自己的Key
|
||
|
||
### 2. 长期措施
|
||
|
||
1. **方案一实施**:在Key格式中嵌入实例标识
|
||
2. **数据库迁移**:为现有激活码/Key添加实例ID字段
|
||
3. **过渡期**:支持新旧格式并行,逐步迁移
|
||
|
||
### 3. 配置建议
|
||
|
||
```yaml
|
||
# config.yaml
|
||
security:
|
||
# 实例标识(用于Key生成)
|
||
instance_id: "your-unique-instance-id"
|
||
|
||
# 是否启用实例验证(升级后启用)
|
||
validate_instance: true
|
||
```
|
||
|
||
## 风险评估
|
||
|
||
| 风险项 | 当前状态 | 影响程度 | 建议优先级 |
|
||
|--------|-----------|----------|------------|
|
||
| 激活码跨实例使用 | 存在 | 高 | 高 |
|
||
| API Key跨实例使用 | 存在 | 高 | 高 |
|
||
| 管理员Key跨实例 | 存在 | 极高 | 紧急 |
|
||
|
||
## 总结
|
||
|
||
当前Sub2API系统在激活码和API Key生成时未包含系统标识,存在跨实例使用风险。建议在后续版本中:
|
||
|
||
1. **短期**:添加日志记录,便于追踪
|
||
2. **长期**:修改Key格式,嵌入实例标识
|
||
|
||
此问题需要开发团队评估影响范围后实施修复。
|
||
|
||
---
|
||
*文档版本:1.0*
|
||
*最后更新:2025-01*
|
||
*分析基于:Sub2API v0.1.104* |