Files
lijiaoqiao/docs/security_solution_v1_2026-03-18.md
2026-03-26 16:04:46 +08:00

667 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全解决方案P0问题修复
> 版本v1.0
> 日期2026-03-18
> 目的系统性解决评审发现的安全P0问题
---
## 1. 计费数据防篡改机制
### 1.1 当前问题
- 只有 usage_records 表,缺乏完整性校验
- 无防篡改审计日志
- 无法追溯数据变更
### 1.2 解决方案
#### 1.2.1 双重记账设计
```python
# 双重记账:借方和贷方必须平衡
class DoubleEntryBilling:
def record_billing(self, transaction: Transaction):
# 1. 借方:用户账户余额
self.debit(
account_type='user_balance',
account_id=transaction.user_id,
amount=transaction.amount,
currency=transaction.currency
)
# 2. 贷方:收入账户
self.credit(
account_type='revenue',
account_id='platform_revenue',
amount=transaction.amount,
currency=transaction.currency
)
# 3. 验证平衡
assert self.get_balance('user', transaction.user_id) + \
self.get_balance('revenue', 'platform_revenue') == 0
```
#### 1.2.2 审计日志表
```sql
-- PostgreSQL 版本:计费审计日志表
CREATE TABLE IF NOT EXISTS billing_audit_log (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
table_name VARCHAR(50) NOT NULL,
operation VARCHAR(20) NOT NULL,
old_value JSONB,
new_value JSONB,
operator_id BIGINT NOT NULL,
operator_ip INET,
operator_role VARCHAR(50),
request_id VARCHAR(64),
record_hash CHAR(64) NOT NULL,
previous_hash CHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_billing_audit_log_record_id
ON billing_audit_log (record_id);
CREATE INDEX IF NOT EXISTS idx_billing_audit_log_operator_id
ON billing_audit_log (operator_id);
CREATE INDEX IF NOT EXISTS idx_billing_audit_log_created_at
ON billing_audit_log (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_billing_audit_log_request_id
ON billing_audit_log (request_id);
-- PostgreSQL 触发器:自动记录变更(示例)
CREATE OR REPLACE FUNCTION fn_audit_supply_usage_update()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_prev_hash CHAR(64);
BEGIN
SELECT record_hash
INTO v_prev_hash
FROM billing_audit_log
WHERE record_id = OLD.id
ORDER BY id DESC
LIMIT 1;
INSERT INTO billing_audit_log (
record_id,
table_name,
operation,
old_value,
new_value,
operator_id,
operator_ip,
operator_role,
request_id,
record_hash,
previous_hash
)
VALUES (
OLD.id,
'supply_usage_records',
'UPDATE',
to_jsonb(OLD),
to_jsonb(NEW),
COALESCE(NULLIF(current_setting('app.operator_id', true), ''), '0')::BIGINT,
NULLIF(current_setting('app.operator_ip', true), '')::INET,
NULLIF(current_setting('app.operator_role', true), ''),
NULLIF(current_setting('app.request_id', true), ''),
encode(digest(to_jsonb(NEW)::text, 'sha256'), 'hex'),
v_prev_hash
);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_usage_before_update ON supply_usage_records;
CREATE TRIGGER trg_usage_before_update
BEFORE UPDATE ON supply_usage_records
FOR EACH ROW
EXECUTE FUNCTION fn_audit_supply_usage_update();
```
#### 1.2.3 实时对账机制
```python
class BillingReconciliation:
def hourly_reconciliation(self):
"""小时级对账"""
# 1. 获取计费记录
billing_records = self.get_billing_records(
start_time=self.hour_ago,
end_time=datetime.now()
)
# 2. 获取用户消费记录
usage_records = self.get_usage_records(
start_time=self.hour_ago,
end_time=datetime.now()
)
# 3. 比对
discrepancies = []
for billing, usage in zip(billing_records, usage_records):
if not self.is_match(billing, usage):
discrepancies.append({
'billing_id': billing.id,
'usage_id': usage.id,
'difference': billing.amount - usage.amount
})
# 4. 告警
if discrepancies:
self.send_alert('billing_discrepancy', discrepancies)
def real_time_verification(self):
"""实时验证(请求级别)"""
# 每个请求完成后立即验证
request = self.get_current_request()
expected_cost = self.calculate_cost(request.usage)
actual_cost = self.get_billing_record(request.id).amount
# 允许0.1%误差
if abs(expected_cost - actual_cost) > expected_cost * 0.001:
raise BillingAccuracyError(f"计费差异: {expected_cost} vs {actual_cost}")
```
---
## 2. 跨租户隔离强化
### 2.1 当前问题
- team_id 和 organization_id 字段存在
- 但缺乏强制验证和行级安全
### 2.2 解决方案
#### 2.2.1 强制租户上下文验证
```python
class TenantContextMiddleware:
def process_request(self, request):
# 1. 从Token提取租户ID
tenant_id = self.extract_tenant_id(request.token)
# 2. 从URL/Header强制验证
if request.tenant_id and request.tenant_id != tenant_id:
raise TenantMismatchError()
# 3. 强制设置租户上下文
request.tenant_id = tenant_id
# 4. 存储到请求上下文
self.set_context('tenant_id', tenant_id)
```
#### 2.2.2 数据库行级安全RLS
```sql
-- 启用行级安全
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
-- 创建策略用户只能访问自己的Key
CREATE POLICY api_keys_tenant_isolation
ON api_keys
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
-- 对所有敏感表启用RLS
ALTER TABLE billing_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE usage_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
```
#### 2.2.3 敏感操作二次验证
```python
class SensitiveOperationGuard:
# 需要二次验证的操作
SENSITIVE_ACTIONS = [
'billing.write', # 写账单
'admin.tenant_write', # 租户管理
'provider.withdraw', # 供应方提现
]
def verify(self, user_id, action, context):
if action not in self.SENSITIVE_ACTIONS:
return True
# 1. 检查用户权限级别
user = self.get_user(user_id)
if user.role == 'admin':
return True
# 2. 检查是否需要二次验证
if self.requires_mfa(action, context):
# 发送验证码
self.send_verification_code(user)
return False
# 3. 记录审计日志
self.audit_log(user_id, action, context)
return True
```
---
## 3. 密钥轮换机制
### 3.1 当前问题
- API Key 无失效机制
- 无法强制轮换
- 无生命周期管理
### 3.2 解决方案
#### 3.2.1 密钥生命周期管理
```python
class APIKeyLifecycle:
# 配置
KEY_EXPIRY_DAYS = 90 # 有效期90天
WARNING_DAYS = 14 # 提前14天提醒
GRACE_PERIOD_DAYS = 7 # 宽限期7天
MAX_KEYS_PER_USER = 10 # 每个用户最多10个Key
def generate_key(self, user_id, description) -> APIKey:
# 1. 检查Key数量限制
current_keys = self.count_user_keys(user_id)
if current_keys >= self.MAX_KEYS_PER_USER:
raise MaxKeysExceededError()
# 2. 生成Key
key = self._generate_key_string()
# 3. 存储Key信息
api_key = APIKey(
key_hash=self.hash(key),
key_prefix=key[:12], # 显示前缀
user_id=user_id,
description=description,
expires_at=datetime.now() + timedelta(days=self.KEY_EXPIRY_DAYS),
created_at=datetime.now(),
status='active',
version=1
)
# 4. 保存到数据库
self.save(api_key)
return api_key
def is_key_valid(self, key: APIKey) -> ValidationResult:
# 1. 检查状态
if key.status == 'disabled':
return ValidationResult(False, 'Key is disabled')
if key.status == 'expired':
return ValidationResult(False, 'Key is expired')
# 2. 检查是否过期
if key.expires_at and key.expires_at < datetime.now():
# 检查是否在宽限期
if key.expires_at > datetime.now() - timedelta(days=self.GRACE_PERIOD_DAYS):
# 在宽限期,提醒但不拒绝
return ValidationResult(True, 'Key expiring soon', warning=True)
return ValidationResult(False, 'Key expired')
# 3. 检查是否需要轮换提醒
days_until_expiry = (key.expires_at - datetime.now()).days
if days_until_expiry <= self.WARNING_DAYS:
# 异步通知用户
self.notify_key_expiring(key, days_until_expiry)
return ValidationResult(True, 'Valid')
```
#### 3.2.2 密钥泄露应急处理
```python
class KeyCompromiseHandler:
def report_compromised(self, key_id, reporter_id):
"""报告Key泄露"""
# 1. 立即禁用Key
key = self.get_key(key_id)
key.status = 'compromised'
key.disabled_at = datetime.now()
key.disabled_by = reporter_id
self.save(key)
# 2. 通知用户
user = self.get_user(key.user_id)
self.send_notification(user, 'key_compromised', {
'key_id': key_id,
'reported_at': datetime.now()
})
# 3. 记录审计日志
self.audit_log('key_compromised', {
'key_id': key_id,
'reported_by': reporter_id,
'action': 'disabled'
})
# 4. 自动创建新Key可选
new_key = self.generate_key(key.user_id, 'Auto-generated replacement')
return new_key
def rotate_key(self, key_id):
"""主动轮换Key"""
old_key = self.get_key(key_id)
# 1. 创建新Key
new_key = self.generate_key(
old_key.user_id,
f"Rotation of {old_key.description}"
)
# 2. 标记旧Key为轮换
old_key.status = 'rotated'
old_key.rotated_at = datetime.now()
old_key.replaced_by = new_key.id
self.save(old_key)
return new_key
```
---
## 4. 激活码安全强化
### 4.1 当前问题
- 6位随机数entropy不足
- MD5校验和可碰撞
### 4.2 解决方案
```python
import secrets
import hashlib
import hmac
class SecureActivationCode:
def generate(self, user_id: int, expiry_days: int) -> str:
# 1. 使用 crypto.random 替代 random
# 16字节 = 128位 entropy
random_bytes = secrets.token_bytes(16)
random_hex = random_bytes.hex()
# 2. 使用 HMAC-SHA256 替代 MD5
expiry = datetime.now() + timedelta(days=expiry_days)
expiry_str = expiry.strftime("%Y%m%d")
# 3. 构建原始字符串
raw = f"lgw-act-{user_id}-{expiry_str}-{random_hex}"
# 4. HMAC 签名(使用应用密钥)
signature = hmac.new(
self.secret_key.encode(),
raw.encode(),
hashlib.sha256
).hexdigest()[:16]
return f"{raw}-{signature}"
def verify(self, code: str) -> VerificationResult:
parts = code.split('-')
if len(parts) != 6:
return VerificationResult(False, 'Invalid format')
# 1. 解析各部分
_, _, user_id, expiry_str, random_hex, signature = parts
# 2. 验证签名
raw = f"lgw-act-{user_id}-{expiry_str}-{random_hex}"
expected_signature = hmac.new(
self.secret_key.encode(),
raw.encode(),
hashlib.sha256
).hexdigest()[:16]
if not hmac.compare_digest(signature, expected_signature):
return VerificationResult(False, 'Invalid signature')
# 3. 验证过期
expiry = datetime.strptime(expiry_str, "%Y%m%d")
if expiry < datetime.now():
return VerificationResult(False, 'Expired')
return VerificationResult(True, 'Valid', user_id=int(user_id))
```
---
## 4. DDoS防护机制
### 4.1 防护层级
```python
class DDoSProtection:
"""DDoS防护 - 修复S-D-01"""
# 三层防护
TIERS = [
{'name': 'L4', 'layer': 'tcp', 'method': 'syn_cookie'},
{'name': 'L7', 'layer': 'http', 'method': 'rate_limit'},
{'name': 'APP', 'layer': 'application', 'method': 'challenge'}
]
# 限流配置
RATE_LIMITS = {
'global': {'requests': 100000, 'window': 60},
'per_ip': {'requests': 1000, 'window': 60},
'per_token': {'requests': 100, 'window': 60},
'burst': {'requests': 50, 'window': 1}
}
# IP黑名单
def check_ip_blacklist(self, ip: str) -> bool:
"""检查IP是否在黑名单"""
return self.redis.sismember('ddos:blacklist', ip)
def add_to_blacklist(self, ip: str, reason: str, duration: int = 3600):
"""加入黑名单"""
self.redis.sadd('ddos:blacklist', ip)
self.redis.expire('ddos:blacklist', duration)
# 记录原因
self.redis.hset('ddos:blacklist:reasons', ip, json.dumps({
'reason': reason,
'added_at': datetime.now().isoformat()
}))
```
### 4.2 攻击检测
```python
class AttackDetector:
"""攻击检测"""
# 检测规则
RULES = {
'syn_flood': {'threshold': 1000, 'window': 10, 'action': 'block'},
'http_flood': {'threshold': 500, 'window': 60, 'action': 'rate_limit'},
'slowloris': {'threshold': 50, 'window': 60, 'action': 'block'},
'credential_stuffing': {'threshold': 100, 'window': 60, 'action': 'challenge'}
}
async def detect(self, metrics: AttackMetrics) -> DetectionResult:
"""检测攻击"""
for rule_name, rule in self.RULES.items():
if metrics.exceeds_threshold(rule):
return DetectionResult(
attack=True,
rule=rule_name,
action=rule['action'],
severity='HIGH' if rule['action'] == 'block' else 'MEDIUM'
)
return DetectionResult(attack=False)
```
---
## 5. 日志脱敏规则
### 5.1 脱敏字段定义
```python
class LogDesensitization:
"""日志脱敏 - 修复S-D-02"""
# 脱敏规则
RULES = {
'api_key': {
'pattern': r'(sk-[a-zA-Z0-9]{20,})',
'replacement': r'sk-***',
'level': 'SENSITIVE'
},
'password': {
'pattern': r'(password["\']?\s*[:=]\s*["\']?)([^"\']+)',
'replacement': r'\1***',
'level': 'SENSITIVE'
},
'email': {
'pattern': r'([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
'replacement': r'\1***@\2',
'level': 'PII'
},
'phone': {
'pattern': r'(1[3-9]\d)(\d{4})(\d{4})',
'replacement': r'\1****\3',
'level': 'PII'
},
'ip_address': {
'pattern': r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})',
'replacement': r'\1 (masked)',
'level': 'NETWORK'
},
'credit_card': {
'pattern': r'(\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4})',
'replacement': r'****-****-****-\4',
'level': 'SENSITIVE'
}
}
def desensitize(self, log: dict) -> dict:
"""脱敏处理"""
import re
result = {}
for key, value in log.items():
if isinstance(value, str):
result[key] = self._desensitize_value(value)
else:
result[key] = value
return result
```
### 5.2 日志级别
```python
class LogLevel:
"""日志级别"""
LEVELS = {
'DEBUG': {'mask': False, 'retention_days': 7},
'INFO': {'mask': False, 'retention_days': 30},
'WARNING': {'mask': False, 'retention_days': 90},
'ERROR': {'mask': False, 'retention_days': 365},
'SENSITIVE': {'mask': True, 'retention_days': 365} # 敏感日志必须脱敏
}
def should_mask(self, level: str) -> bool:
"""是否需要脱敏"""
return self.LEVELS.get(level, {}).get('mask', False)
```
---
## 6. 密钥定期轮换
### 6.1 定期轮换策略
```python
class KeyRotationScheduler:
"""密钥定期轮换 - 修复S-D-03"""
# 轮换配置
ROTATION_CONFIG = {
'api_key': {'days': 90, 'warning_days': 14},
'internal_key': {'days': 30, 'warning_days': 7},
'provider_key': {'days': 60, 'warning_days': 10}
}
async def schedule_rotation(self):
"""调度轮换"""
while True:
# 1. 查找需要轮换的Key
keys_due = await self.find_keys_due_for_rotation()
# 2. 发送提醒
for key in keys_due:
await self.send_rotation_warning(key)
# 3. 自动轮换(超过宽限期)
keys_expired = await self.find_expired_keys()
for key in keys_expired:
await self.auto_rotate(key)
await asyncio.sleep(3600) # 每小时检查
async def auto_rotate(self, key: APIKey):
"""自动轮换"""
# 1. 创建新Key
new_key = await self.generate_key(key.user_id, key.description)
# 2. 标记旧Key
key.status = 'rotating'
key.rotated_at = datetime.now()
key.replaced_by = new_key.id
# 3. 通知用户
await self.notify_user(key.user_id, {
'type': 'key_rotated',
'old_key_id': key.id,
'new_key': new_key.key_prefix + '***'
})
```
---
## 7. 实施计划
### 7.1 优先级
| 任务 | 负责人 | 截止 | 依赖 |
|------|--------|------|------|
| 计费防篡改机制 | 后端 | S1前 | - |
| 跨租户隔离强化 | 架构 | S1前 | - |
| 密钥轮换机制 | 后端 | S0-M1 | - |
| 激活码安全强化 | 后端 | S0-M1 | - |
| DDoS防护机制 | 安全 | S0-M2 | - |
| 日志脱敏规则 | 后端 | S0-M1 | - |
| 密钥定期轮换 | 后端 | S0-M2 | - |
### 7.2 验证标准
- 所有计费操作都有审计日志
- 跨租户访问被强制拦截
- Key可以正常轮换和失效
- 激活码无法伪造
- DDoS攻击可被检测和阻断
- 敏感日志自动脱敏
---
**文档状态**:安全解决方案(修复版)
**关联文档**
- `security_api_key_vulnerability_analysis_v1_2026-03-18.md`
- `supply_detailed_design_v1_2026-03-18.md`