Files
lijiaoqiao/docs/security_solution_v1_2026-03-18.md
2026-03-26 20:06:14 +08:00

18 KiB
Raw Blame History

安全解决方案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.md
  • supply_detailed_design_v1_2026-03-18.md