667 lines
18 KiB
Markdown
667 lines
18 KiB
Markdown
# 安全解决方案(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`
|