forked from xiaohei/taiji-AI-PAD
fix(mcp-server): two audit-found register bugs
Found by post-fix audit pass over this session's changes: 1. Predcheck used `effective_username = (req.username or "").strip()` but the write at the User() construction site still used `req.username` raw. If a client sent "alice " with trailing whitespace, predcheck queried for "alice" (clean), missed the conflict, then wrote "alice " back. Now both sites use the same `effective_username` value — single source of truth. 2. Post-commit `verify_code` was guarded by `except Exception`, but `asyncio.CancelledError` is a BaseException and propagates through. If the request task is cancelled (client disconnect / pod shutdown) after DB commit but before verify_code finishes, the verification code stays in Redis with full 10-min TTL. Wrapped with `asyncio.shield(...)` so verify_code completes regardless of cancellation, and an explicit `except CancelledError: raise` preserves FastAPI's cancellation semantics for the outer request. Verified via smoke: - Register with username "ws_user_$ts " (trailing spaces) → DB stores "ws_user_$ts" (18 chars, no whitespace). Predcheck and write now agree. - P4 透传 (balance/models/usage/logs via 55@55.com) still 4/4 — no regression. Latent bug noticed but NOT introduced this session, deferred: - auth.py:981 writes ResourceAllocation.resource_id=str(provider.id) for model allocations, but channel.py:1881 queries by resource_id==model_name. Pre-existing inconsistency means update_tenant_model_quota never finds rows created at register time. TenantModelKey row is still updated correctly so end-user quota is honored; only the ResourceAllocation audit/reporting view diverges. Fix requires deciding which side is canonical — out of scope for security hardening. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -779,7 +779,9 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
# 创建新用户
|
||||
password_hash = get_password_hash(req.password)
|
||||
username = req.username or req.email.split("@")[0]
|
||||
# 用同一套 effective_username 写入 —— line 745 的预检和这里的写入必须 100%
|
||||
# 匹配,否则带空格的 username(如 "alice ")会预检通过但写入不一致。
|
||||
username = effective_username
|
||||
name = req.full_name or username
|
||||
|
||||
new_user = User(
|
||||
@@ -1053,8 +1055,17 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# DB commit 已成功 —— 最后才真正消费验证码。这一步即使失败也不再回滚
|
||||
# (用户已经注册成功,重复消费没意义;Redis 里的过期码不会被复用,因为
|
||||
# 同 email 第二次 register 会在 line 737 的 existing_user 检查处 400)。
|
||||
#
|
||||
# 用 asyncio.shield 保护 verify_code 不被 client disconnect / task cancel
|
||||
# 半道杀掉 —— 否则 CancelledError 是 BaseException 直接穿透 except
|
||||
# Exception,Redis 里残留 10min TTL 的码(不影响安全,但占资源)。
|
||||
import asyncio # local import 避免 module-level 顺序问题
|
||||
try:
|
||||
await verify_code(req.email, req.verification_code)
|
||||
await asyncio.shield(verify_code(req.email, req.verification_code))
|
||||
except asyncio.CancelledError:
|
||||
# request 已经被取消,shield 内部的 verify_code 还会跑完。重新抛出,
|
||||
# 让 FastAPI 走正常取消流程。
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.warning("注册成功后消费验证码失败(不影响注册结果)",
|
||||
email=req.email, error=str(e))
|
||||
|
||||
Reference in New Issue
Block a user