fix: PIN brute-force defense — IP-scoped backoff + close user-existence oracle#73
Merged
Merged
Conversation
PIN lockout was keyed by the unauthenticated user_id from the request, so anyone who knew a user_id (all of which /profiles lists) could fail PINs to lock a specific household member out -- an account-lockout DoS -- and the differing 404/403 responses leaked whether a user existed. - Rate-limit PIN attempts per (client IP, user_id) with exponential backoff (30s, doubling, capped at 1h) instead of a flat per-user lock. Attacker and victim have different IPs, so an attacker can no longer lock out the victim; a shared device can still switch to another profile while one is backed off. - Derive the client IP from the socket peer by default; honor X-Forwarded-For (rightmost hop) only when the new trust_proxy_headers setting is enabled. - Make /select uniform whether or not the user exists: a missing user is verified against a dummy scrypt hash and returns the same 403 + timing as a wrong PIN (no more 404 "User not found"), and repeated probes back off too. - Emit Retry-After on 429. Throttle state stays in-process (no Redis needed for tests/local dev); a TODO notes moving it to the existing Redis for multi-worker + restart persistence. Tests: per-IP isolation, exponential backoff/cap, oracle indistinguishability (wrong PIN vs unknown user, with and without a PIN), reset-on-success and reset-on-PIN-change, plus client-IP/XFF derivation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardens PIN brute-force defense (roadmap LATER tier, #11). Additive — no schema/migration.
Problem
PIN lockout was keyed by the unauthenticated
user_idfrom the request, so anyone reachable could repeatedly fail PINs to lock a specific household member out (account-lockout DoS)./selectalso returned 404 for missing users, making it a user-existence oracle.Fix
429now carriesRetry-After.request.client.host(unspoofable socket peer) by default; opt-inTRUST_PROXY_HEADERSuses the rightmostX-Forwarded-Forhop (client-forgeable leftmost entries never trusted).Verification (local)
pytest -q→ 155 passed (+test_pin_throttle.py9 tests;test_pin_lockout_is_scoped_to_client_ip,test_select_is_not_a_user_existence_oracle, and the 429-burst test updated with aRetry-Afterassertion).ruff+mypyclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY