fix(saas): P1 审计修复 — 连接池断路器 + Worker重试 + XSS防护 + 状态机SQL解析器

P1 修复内容:
- F7: health handler 连接池容量检查 (80%阈值返回503 degraded)
- F9: SSE spawned task 并发限制 (Semaphore 16 permits)
- F10: Key Pool 单次 JOIN 查询优化 (消除 N+1)
- F12: CORS panic → 配置错误
- F14: 连接池使用率计算修正 (ratio = used*100/total)
- F15: SQL 迁移解析器替换为状态机 (支持 $$, DO $body$, 存储过程)
- Worker 重试机制: 失败任务通过 mpsc channel 重新入队
- DOMPurify XSS 防护 (PipelineResultPreview)
- Admin V2: ErrorBoundary + SWR全局配置 + 请求优化
This commit is contained in:
iven
2026-03-30 14:21:39 +08:00
parent bc8c77e7fe
commit ba2c6a6105
38 changed files with 490 additions and 236 deletions

View File

@@ -148,6 +148,34 @@ pub async fn verify_totp(
return Err(SaasError::InvalidInput("TOTP 码必须是 6 位数字".into()));
}
// TOTP 暴力破解保护: 10 分钟内最多 5 次失败
const MAX_TOTP_FAILURES: u32 = 5;
const TOTP_LOCKOUT_SECS: u64 = 600;
let now = std::time::Instant::now();
let lockout_duration = std::time::Duration::from_secs(TOTP_LOCKOUT_SECS);
let is_locked = {
if let Some(entry) = state.totp_fail_counts.get(&ctx.account_id) {
let (count, first_fail) = entry.value();
if *count >= MAX_TOTP_FAILURES && now.duration_since(*first_fail) < lockout_duration {
true
} else {
// 窗口过期,重置
drop(entry);
state.totp_fail_counts.remove(&ctx.account_id);
false
}
} else {
false
}
};
if is_locked {
return Err(SaasError::RateLimited(
format!("TOTP 验证失败次数过多,请 {} 秒后重试", TOTP_LOCKOUT_SECS)
));
}
// 获取存储的密钥
let (totp_secret,): (Option<String>,) = sqlx::query_as(
"SELECT totp_secret FROM accounts WHERE id = $1"
@@ -172,9 +200,24 @@ pub async fn verify_totp(
};
if !verify_totp_code(&secret, code) {
// 记录失败次数
let new_count = {
let mut entry = state.totp_fail_counts
.entry(ctx.account_id.clone())
.or_insert((0, now));
entry.value_mut().0 += 1;
entry.value().0
};
tracing::warn!(
"TOTP verify failed for account {} ({}/{} attempts)",
ctx.account_id, new_count, MAX_TOTP_FAILURES
);
return Err(SaasError::Totp("TOTP 码验证失败".into()));
}
// 验证成功 → 清除失败计数
state.totp_fail_counts.remove(&ctx.account_id);
// 验证成功 → 启用 TOTP同时确保密钥已加密
let final_secret = if encrypted_secret.starts_with(crypto::ENCRYPTED_PREFIX) {
encrypted_secret
@@ -183,10 +226,10 @@ pub async fn verify_totp(
encrypt_totp_secret(&secret, &enc_key)?
};
let now = chrono::Utc::now().to_rfc3339();
let now_ts = chrono::Utc::now().to_rfc3339();
sqlx::query("UPDATE accounts SET totp_enabled = true, totp_secret = $1, updated_at = $2 WHERE id = $3")
.bind(&final_secret)
.bind(&now)
.bind(&now_ts)
.bind(&ctx.account_id)
.execute(&state.db)
.await?;