Skip to content

fix(auth): harden login/session handling (#334) - #355

Merged
ajianaz merged 5 commits into
developfrom
fix/334-auth-hardening
Oct 13, 2026
Merged

ajianaz merged 5 commits into
developfrom
fix/334-auth-hardening

Conversation

@ajianaz

@ajianaz ajianaz commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Refs #334 (partial: login/session items; remaining items listed below, so not auto-closing)

What

  • Client IP for login rate limiting is the TCP peer address; X-Forwarded-For / X-Real-IP are used only when TRAPFALL_TRUST_PROXY=true (rightmost XFF entry).
  • Lockout: per (email, IP) 5 fails/15 min, plus a global per-email ceiling of 30 fails/15 min across all IPs (bounds IP-rotating guessing), plus a per-IP cap of 100 (raised from 20 so a shared proxy IP cannot be trivially used to lock everyone out). A warning is logged once when proxy headers arrive from a private/loopback peer while TRAPFALL_TRUST_PROXY is unset.
    Trade-off: an attacker with 30+ distinct IPs can still lock an account for the rest of the window; a single IP cannot.
  • Argon2 hash/verify run via spawn_blocking; unknown-email logins burn one dummy verify to equalise timing.
  • First-admin setup is atomic (single INSERT ... WHERE NOT EXISTS; Postgres adds an advisory xact lock).
  • Session tokens are stored as h1:<blake3>; legacy plaintext rows are purged at startup.
  • change_password revokes all other sessions (keeps the caller's).
  • New DB trait methods: count_failed_attempts_email_ip, create_first_user, delete_user_sessions_except, purge_sessions_without_prefix (SQLite + Postgres).

Why

Security audit findings in #334 (spoofable IP limit, lockout DoS, Argon2 stalling the single-threaded runtime, setup race, plaintext session tokens, sessions surviving password change).

Breaking / upgrade notes

  • Behind a reverse proxy, set TRAPFALL_TRUST_PROXY=true or all clients share the proxy IP for rate limiting.
  • All existing sessions are invalidated once on upgrade (users log in again).
  • Config has a new trust_proxy field.

Deferred (not in this PR): ingest rate limiter keyed by attacker-chosen project_id / unbounded map; DSN host from Host header when public_url unset; setup token printed at startup; docker-compose Secure cookie default (config default is already true).

Testing

  • New tests: per-IP lockout isolation, atomic concurrent setup, hashed session storage, legacy purge, session invalidation on password change (unit + HTTP), XFF spoofing ignored, rightmost-XFF with trusted proxy, Argon2 not blocking a current_thread runtime.
  • cargo fmt --check: OK
  • cargo clippy --workspace --all-targets -- -D warnings: clean
  • cargo test --workspace: all suites pass (0 failures)

🤖 Generated with Claude Code

ajianaz and others added 5 commits October 13, 2026 08:37
- trust X-Forwarded-For only when TRAPFALL_TRUST_PROXY is set
- per-(email,IP) lockout so remote attackers cannot lock out the admin
- run Argon2 on the blocking pool; equalise timing for unknown users
- atomic first-admin setup
- store hashed session tokens, purge legacy plaintext sessions
- change_password revokes other sessions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- add global per-email ceiling (30 fails/15min across all IPs)
- raise per-IP cap to 100 and warn once when proxy headers arrive
  from a private/loopback peer while TRAPFALL_TRUST_PROXY is unset

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ajianaz
ajianaz merged commit f27f4c2 into develop Oct 13, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant