services: throttle emailed codes to stop RESEND/RESETPASS email-bombing
All checks were successful
CI / check (push) Successful in 3m46s
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:
parent
320a591ae3
commit
61005f52f5
7 changed files with 44 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -983,6 +983,7 @@ pub struct HostConfig {
|
|||
struct PendingCode {
|
||||
kind: CodeKind,
|
||||
code: String,
|
||||
issued: Instant,
|
||||
deadline: Instant,
|
||||
tries_left: u8,
|
||||
}
|
||||
|
|
@ -996,6 +997,10 @@ struct AuthThrottle {
|
|||
// Wrong-code guesses tolerated before a code is invalidated (defence in depth on
|
||||
// top of the code's own entropy).
|
||||
const CODE_TRIES: u8 = 5;
|
||||
|
||||
// Minimum gap between emailed codes for one account, so RESEND/RESETPASS can't be
|
||||
// used to email-bomb an address (or burn the service's sender reputation).
|
||||
const CODE_ISSUE_COOLDOWN: Duration = Duration::from_secs(60);
|
||||
// Free password attempts before the exponential backoff kicks in, and its cap.
|
||||
const AUTH_FREE_TRIES: u32 = 3;
|
||||
const AUTH_MAX_BACKOFF_SECS: u64 = 300;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ impl Store for Db {
|
|||
fn vhost_request_wait(&self, account: &str) -> u64 {
|
||||
Db::vhost_request_wait(self, account)
|
||||
}
|
||||
fn code_issue_wait(&self, account: &str) -> u64 {
|
||||
Db::code_issue_wait(self, account)
|
||||
}
|
||||
fn take_vhost_request(&mut self, account: &str) -> Result<Option<String>, RegError> {
|
||||
Db::take_vhost_request(self, account)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue