fix(redis): INCR+EXPIRE atomar via Pipeline (MULTI/EXEC)
Login-, TOTP- und Kiosk-PIN-Lockout zählten Fehlversuche mit separaten INCR/EXPIRE-Calls. Zwischen beiden konnte der Fail-Counter-Key kurzzeitig ohne TTL bestehen bleiben (Crash-Fenster) - nicht atomar. Jetzt per redis.pipeline(transaction=True) als MULTI/EXEC. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
This commit is contained in:
@@ -278,8 +278,12 @@ async def _record_totp_failure(user_id: str, redis) -> None:
|
||||
"""Zählt TOTP-Fehlversuch und setzt Lockout nach TOTP_MAX_ATTEMPTS Fehlversuchen."""
|
||||
fail_key = f"totp_fails:{user_id}"
|
||||
lock_key = f"totp_lockout:{user_id}"
|
||||
fails = await redis.incr(fail_key)
|
||||
await redis.expire(fail_key, TOTP_LOCKOUT_SECONDS)
|
||||
# INCR+EXPIRE atomar (MULTI/EXEC) – sonst könnte fail_key zwischen beiden
|
||||
# Calls kurzzeitig ohne TTL bestehen (Crash-Fenster).
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
pipe.incr(fail_key)
|
||||
pipe.expire(fail_key, TOTP_LOCKOUT_SECONDS)
|
||||
fails, _ = await pipe.execute()
|
||||
if fails >= TOTP_MAX_ATTEMPTS:
|
||||
await redis.set(lock_key, "1", ex=TOTP_LOCKOUT_SECONDS)
|
||||
await redis.delete(fail_key)
|
||||
|
||||
Reference in New Issue
Block a user