18 KiB
18 KiB
安全解决方案(P0问题修复)
版本:v1.0 日期:2026-03-18 目的:系统性解决评审发现的安全P0问题
1. 计费数据防篡改机制
1.1 当前问题
- 只有 usage_records 表,缺乏完整性校验
- 无防篡改审计日志
- 无法追溯数据变更
1.2 解决方案
1.2.1 双重记账设计
# 双重记账:借方和贷方必须平衡
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 审计日志表
-- 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 实时对账机制
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 强制租户上下文验证
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)
-- 启用行级安全
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 敏感操作二次验证
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 密钥生命周期管理
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 密钥泄露应急处理
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 解决方案
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 防护层级
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 攻击检测
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 脱敏字段定义
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 日志级别
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 定期轮换策略
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.mdsupply_detailed_design_v1_2026-03-18.md