fix(2fa): per-account verify lockout and tighter TOTP skew (BUNYIP-201) #243
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/bunyip-201-2fa-per-account-lockout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
POST /v1/auth/2fa/verify was throttled only per source IP (5/min via RateLimitConfig::LOGIN), so an attacker rotating cheap proxy IPs against one victim's challenge token faced no aggregate cap, and the 6-digit TOTP was checked with skew=1 (3 valid codes live at once), giving ~3/10^6 per guess. Combined that put a likely 2FA bypass within roughly half an hour with no lockout.
Add a per-account failed-attempt counter (RateLimitConfig::TWO_FACTOR_VERIFY_FAILURES, 5 failures / 15 min) keyed on 2fa_verify_user:{user_id}, independent of source IP. It is read read-only before code verification (compared with >= so the boundary blocks and a possibly-correct attempt does not spend the budget), incremented only on a failed code, and reset on success, so a legitimate user is never throttled but once the cap is hit the account's 2FA verification is locked for the rest of the window (even a correct code is refused). Counter writes are best-effort so a DB hiccup cannot change the auth outcome or turn an invalid-code response into a 500.
Reduce the TOTP skew from 1 to 0 in build_totp/check_code so only the current 30s step is accepted, removing the adjacent-window codes and tripling per-guess difficulty.
#BUNYIP-201
POST /v1/auth/2fa/verify was throttled only per source IP (5/min via RateLimitConfig::LOGIN), so an attacker rotating cheap proxy IPs against one victim's challenge token faced no aggregate cap, and the 6-digit TOTP was checked with skew=1 (3 valid codes live at once), giving ~3/10^6 per guess. Combined that put a likely 2FA bypass within roughly half an hour with no lockout. Add a per-account failed-attempt counter (RateLimitConfig::TWO_FACTOR_VERIFY_FAILURES, 5 failures / 15 min) keyed on 2fa_verify_user:{user_id}, independent of source IP. It is read read-only before code verification (compared with >= so the boundary blocks and a possibly-correct attempt does not spend the budget), incremented only on a failed code, and reset on success, so a legitimate user is never throttled but once the cap is hit the account's 2FA verification is locked for the rest of the window (even a correct code is refused). Counter writes are best-effort so a DB hiccup cannot change the auth outcome or turn an invalid-code response into a 500. Reduce the TOTP skew from 1 to 0 in build_totp/check_code so only the current 30s step is accepted, removing the adjacent-window codes and tripling per-guess difficulty. #BUNYIP-201