M-02: Timing-safe login prevents username enumeration
When a login targets a non-existent username, run Argon2id against a pre-computed dummy hash so response time (~60ms) matches wrong-password attempts. Also reorder the login flow to run verify_password_with_upgrade BEFORE the lockout check, preventing timing side-channels that could distinguish locked accounts from wrong passwords. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2f58282c31
commit
8e27f2920b
@ -36,6 +36,7 @@ from app.schemas.auth import (
|
||||
)
|
||||
from app.services.auth import (
|
||||
hash_password,
|
||||
verify_password,
|
||||
verify_password_with_upgrade,
|
||||
create_session_token,
|
||||
verify_session_token,
|
||||
@ -47,6 +48,12 @@ from app.config import settings as app_settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Pre-computed dummy hash for timing equalization (M-02).
|
||||
# When a login attempt targets a non-existent username, we still run
|
||||
# Argon2id verification against this dummy hash so the response time
|
||||
# is indistinguishable from a wrong-password attempt.
|
||||
_DUMMY_HASH = hash_password("timing-equalization-dummy")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cookie helper
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -287,12 +294,17 @@ async def login(
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
# M-02: Run Argon2id against a dummy hash so the response time is
|
||||
# indistinguishable from a wrong-password attempt (prevents username enumeration).
|
||||
verify_password("x", _DUMMY_HASH)
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
|
||||
await _check_account_lockout(user)
|
||||
|
||||
# M-02: Run password verification BEFORE lockout check so Argon2id always
|
||||
# executes — prevents distinguishing "locked" from "wrong password" via timing.
|
||||
valid, new_hash = verify_password_with_upgrade(data.password, user.password_hash)
|
||||
|
||||
await _check_account_lockout(user)
|
||||
|
||||
if not valid:
|
||||
await _record_failed_login(db, user)
|
||||
await log_audit_event(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user