From 34892dbba7610f8cc566da6d6fa5c50d88731f8d Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 13 Jul 2026 23:39:49 +0000 Subject: [PATCH] Harden gRPC startup and the votekick tally map - Refuse to start the gRPC accounts API on an empty token: the constant-time compare treats two empty slices as equal, so a blank token would let an unauthenticated request through. Mirror the guard the JSON-RPC endpoint already had. - Sweep expired votekick/voteban tallies on each vote. A passed vote removed its own key, but a tally that never reached threshold lingered forever, so the map could be grown without bound. The now-redundant per-key TTL reset goes away. --- src/engine/mod.rs | 8 +++++--- src/grpc.rs | 6 ++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 09d295d..7928936 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1295,10 +1295,12 @@ impl Engine { let now = self.now_secs(); let key = (channel.to_ascii_lowercase(), target_display.to_ascii_lowercase()); + // Sweep tallies past their window before touching the map: a passed vote + // deletes its own key, but one that never reaches threshold would linger + // forever otherwise, so the map would grow without bound under abuse. + self.votes.retain(|_, vs| now.saturating_sub(vs.started) <= VOTE_TTL); + let vs = self.votes.entry(key.clone()).or_insert(VoteState { ban, voters: std::collections::HashSet::new(), started: now }); - if now.saturating_sub(vs.started) > VOTE_TTL { - *vs = VoteState { ban, voters: std::collections::HashSet::new(), started: now }; - } vs.ban = ban; vs.voters.insert(from.to_string()); let count = vs.voters.len() as u16; diff --git a/src/grpc.rs b/src/grpc.rs index 057f945..82fe790 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -338,6 +338,12 @@ pub async fn run(engine: Shared, cfg: GrpcCfg, outbound: broadcast::Sender a, Err(e) => return tracing::error!(%e, bind = %cfg.bind, "bad grpc bind address"), }; + // An empty token would make the constant-time compare pass for a request + // that sends no authorization header at all, exposing the account-authority + // API unauthenticated. Refuse to start rather than serve it open. + if cfg.token.is_empty() { + return tracing::error!("grpc token is empty; refusing to start an unauthenticated accounts API"); + } let has_tls = cfg.tls.is_some(); let accounts_svc = AccountsService { engine: engine.clone(), token: cfg.token.clone() }; let stats_svc = StatsService { engine: engine.clone(), token: cfg.token.clone() };