Throttle every password-verify path (GROUP/GHOST/CERT/DROP/SASL) and key the lockout by resolved account

This commit is contained in:
Jean Chevronnet 2026-07-19 13:06:50 +00:00
parent 1ecb76affb
commit 660362a34b
No known key found for this signature in database
7 changed files with 79 additions and 11 deletions

View file

@ -302,7 +302,10 @@ impl Db {
/// Seconds the caller must wait before another password attempt on `account`,
/// or None if it is not currently throttled.
pub fn auth_lockout(&self, account: &str) -> Option<u64> {
let until = self.auth_fails.get(&key(account))?.locked_until?;
// Key the throttle by the resolved account, so every grouped-nick alias of
// one account shares a single lockout bucket (else each spelling is its own).
let k = self.resolved_key(account).unwrap_or_else(|| key(account));
let until = self.auth_fails.get(&k)?.locked_until?;
let now = Instant::now();
(until > now).then(|| (until - now).as_secs() + 1)
}
@ -311,7 +314,7 @@ impl Db {
/// the counter; each failure past a few free tries grows an exponential
/// backoff, throttling guessing without a hard lockout a griefer could abuse.
pub fn note_auth(&mut self, account: &str, success: bool) {
let k = key(account);
let k = self.resolved_key(account).unwrap_or_else(|| key(account));
if success {
self.auth_fails.remove(&k);
return;

View file

@ -314,6 +314,9 @@ impl Engine {
actions
}
AuthThen::Sasl { agent, client, account } => {
// Feed the same brute-force throttle IDENTIFY uses: success clears it,
// a failure grows the backoff — so SASL isn't a throttle-free door.
self.db.note_auth(&account, ok);
if ok {
self.sasl_login("SASL PLAIN", &agent, &client, account)
} else {

View file

@ -78,6 +78,13 @@ impl Engine {
}]
}
Some((authcid, passwd)) => match self.scram_verifier(&authcid) {
// Refuse while throttled, so SASL PLAIN can't be used to
// brute-force past the IDENTIFY lockout.
Some((account, _)) if self.db.auth_lockout(&account).is_some() => {
let mut out = mk("D", vec!["F".to_string()]);
out.extend(self.auth_report(false, Some(&account), "SASL PLAIN", &client, Some("rate limited")));
out
}
Some((account, verifier)) => vec![NetAction::DeferAuthenticate {
verifier,
password: passwd,