Bound registrations with a per-IP quota instead of failure backoff (B-58)

The registration throttle called record_failure on every attempt, successful
ones included. Five legitimate signups from one shared or NAT address locked the
sixth real user out for up to 600s, doubling from there — while an attacker
sidestepped the limiter entirely through B-54. Failure backoff is the wrong
instrument here: nothing about creating an account is a failed guess at a
secret, so the only people it reliably punished were the honest ones.

RollingQuota says what was actually meant: 5 accounts per IP per hour, in a
rolling window. The caller over it waits exactly until the oldest of the five
ages out — an accurate Retry-After, and waiting never makes the next wait
longer. It is recorded only once an account exists, so attempts that create
nothing (a taken username, a validation error) leave the quota untouched, and
checked before the Argon2 hash, so an IP out of quota costs nothing to refuse.

Bounded like the failure limiter (B-56): the keys are caller-chosen, so the dict
gets both a sweep and a hard cap, evicting keys with room left in their quota
before full ones.

Also fixes the inline comment that cited B-31 (the resubscribe finding) where it
meant B-33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Opus 5 committed 2026-08-03 22:32:20 +02:00
1 parent 57721355f0
commit 0aac73e557
6 files changed
+183 -25

No files matched your search

-14
View File
@@ -40,20 +40,6 @@ remains the last prerequisite for running unattended.
## High — security
### B-58 — the registration throttle counts successes as failures and is IP-only
`app/auth/routes.py:66-71`.
`record_failure` is called on every registration attempt, successful ones
included. Five legitimate signups from one shared/NAT address lock the sixth real
user out with exponential backoff up to 600 s — while an attacker skips the limiter
entirely through B-54. The intent (bounding accounts per source) is reasonable;
the current shape punishes only honest users. The inline comment also cites B-31
(the resubscribe finding) where it means B-33.
Fix: keep an accounts-per-IP quota if that is the goal, but express it as a quota
rather than as failure backoff, and fix the B-nn reference.
### B-59 — deposit crediting is not corroborated, unlike external-spend detection
`app/deposits/service.py:31-45`, `app/electrum/listener.py:401-433`.