Harden deferred audit LOWs: provisioned-verifier cost cap, BOT-nick validation, gameserv board-delivery account guard, empty KICK/PART/QUIT reason parse, and relink SASL/enforce cleanup
All checks were successful
CI / check (push) Successful in 5m4s
All checks were successful
CI / check (push) Successful in 5m4s
This commit is contained in:
parent
1b569fdfa3
commit
4027242c1e
13 changed files with 144 additions and 19 deletions
|
|
@ -1146,6 +1146,14 @@ impl Engine {
|
|||
self.bot_channels.clear();
|
||||
self.network.clear_bots();
|
||||
self.network.clear_servers(); // the burst re-introduces the server tree
|
||||
// A re-link's burst re-introduces every user and channel, but the
|
||||
// half-finished SASL exchanges and armed nick-enforcement timers from the
|
||||
// dropped connection are orphaned — forget them so a re-link (were one ever
|
||||
// added in-process) can't act on stale connection state. Today the process
|
||||
// exits on uplink loss and systemd restarts it, so this is defensive.
|
||||
self.sasl_sessions.clear();
|
||||
self.sasl_source.clear();
|
||||
self.pending_enforce.clear();
|
||||
self.next_bot_index = 0;
|
||||
let mut out = vec![NetAction::Burst];
|
||||
for svc in &self.services {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ impl Engine {
|
|||
return AuthorityStatus::Invalid;
|
||||
}
|
||||
}
|
||||
// The verifiers are attacker-influenced if the authority is compromised:
|
||||
// reject an absurd iteration count that would DoS later logins.
|
||||
if !super::scram::verifier_ok(scram256) || (!scram512.is_empty() && !super::scram::verifier_ok(scram512)) {
|
||||
return AuthorityStatus::Invalid;
|
||||
}
|
||||
match self.db.provision_account(name, scram256, scram512, email) {
|
||||
Ok(()) => AuthorityStatus::Ok,
|
||||
Err(RegError::Exists) => AuthorityStatus::AlreadyExists,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,16 @@ pub fn parse_verifier(encoded: &str) -> Option<Verifier> {
|
|||
})
|
||||
}
|
||||
|
||||
// An externally supplied verifier (a gRPC Provision backfill) is untrusted for
|
||||
// its cost parameter: a huge `i=` would make every later PBKDF2 verify burn a
|
||||
// core — a compromised-authority DoS. Accept only a well-formed verifier whose
|
||||
// iteration count is in a sane range (the floor matches the config minimum, the
|
||||
// ceiling is a few multiples of the default so a real backfill still fits).
|
||||
pub const MAX_VERIFIER_ITERATIONS: u32 = 5_000_000;
|
||||
pub fn verifier_ok(encoded: &str) -> bool {
|
||||
parse_verifier(encoded).is_some_and(|v| (1000..=MAX_VERIFIER_ITERATIONS).contains(&v.iterations))
|
||||
}
|
||||
|
||||
// --- exchange ---
|
||||
|
||||
pub struct ClientFirst {
|
||||
|
|
@ -351,4 +361,19 @@ mod tests {
|
|||
assert!(prove(PASSWORD).is_some(), "the real password authenticates against the authority's verifier");
|
||||
assert!(prove("wrong").is_none(), "a wrong password is rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifier_ok_rejects_absurd_iteration_cost() {
|
||||
// Derive one cheap verifier, then swap the `i=` to probe the range — never
|
||||
// actually run PBKDF2 at an absurd count (that IS the DoS we're bounding).
|
||||
let cheap = make_verifier(Hash::Sha256, "pw", 4096);
|
||||
assert!(verifier_ok(&cheap));
|
||||
let with_iters = |i: &str| cheap.replacen("i=4096", &format!("i={i}"), 1);
|
||||
// A huge iteration count (compromised-authority CPU DoS) is refused.
|
||||
assert!(!verifier_ok(&with_iters("4294967295")));
|
||||
assert!(!verifier_ok(&with_iters(&(MAX_VERIFIER_ITERATIONS + 1).to_string())));
|
||||
// A too-cheap or malformed verifier is refused.
|
||||
assert!(!verifier_ok(&with_iters("100")));
|
||||
assert!(!verifier_ok("not a verifier"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue