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 (
|
from app.services.auth import (
|
||||||
hash_password,
|
hash_password,
|
||||||
|
verify_password,
|
||||||
verify_password_with_upgrade,
|
verify_password_with_upgrade,
|
||||||
create_session_token,
|
create_session_token,
|
||||||
verify_session_token,
|
verify_session_token,
|
||||||
@ -47,6 +48,12 @@ from app.config import settings as app_settings
|
|||||||
|
|
||||||
router = APIRouter()
|
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
|
# Cookie helper
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -287,12 +294,17 @@ async def login(
|
|||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not user:
|
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")
|
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)
|
valid, new_hash = verify_password_with_upgrade(data.password, user.password_hash)
|
||||||
|
|
||||||
|
await _check_account_lockout(user)
|
||||||
|
|
||||||
if not valid:
|
if not valid:
|
||||||
await _record_failed_login(db, user)
|
await _record_failed_login(db, user)
|
||||||
await log_audit_event(
|
await log_audit_event(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user