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:
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user