services: throttle emailed codes to stop RESEND/RESETPASS email-bombing
All checks were successful
CI / check (push) Successful in 3m46s

REGISTER was rate-limited but RESEND and RESETPASS weren't — anyone could
repeatedly request confirmation/reset codes for any account with an address
on file, flooding the victim's inbox and burning the service's sender
reputation. A per-account 60s cooldown (code_issue_wait, mirroring the vhost
request throttle) now gates both paths; guessing was already infeasible
(2^40 code + 5-try limit), so this closes the abuse, not an auth hole.
This commit is contained in:
Jean Chevronnet 2026-07-16 10:41:52 +00:00
parent 320a591ae3
commit 61005f52f5
No known key found for this signature in database
7 changed files with 44 additions and 1 deletions

View file

@ -177,11 +177,20 @@ impl Db {
let code = gen_code();
self.codes.insert(
key(account),
PendingCode { kind, code: code.clone(), deadline: Instant::now() + Duration::from_secs(900), tries_left: CODE_TRIES },
PendingCode { kind, code: code.clone(), issued: Instant::now(), deadline: Instant::now() + Duration::from_secs(900), tries_left: CODE_TRIES },
);
code
}
/// Seconds the caller must wait before another emailed code for `account`
/// (0 = allowed now). Throttles RESEND/RESETPASS against email-bombing.
pub fn code_issue_wait(&self, account: &str) -> u64 {
self.codes.get(&key(account)).map_or(0, |pc| {
let ready = pc.issued + CODE_ISSUE_COOLDOWN;
ready.checked_duration_since(Instant::now()).map_or(0, |d| d.as_secs() + 1)
})
}
/// Consume a code for `account`: true if the purpose and code match and it
/// hasn't expired. A wrong guess burns a try and the code is dropped once
/// they run out, so it can't be ground down online.