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.
This commit is contained in:
Jean Chevronnet 2026-07-13 23:39:49 +00:00
parent 5d67133295
commit 34892dbba7
No known key found for this signature in database
2 changed files with 11 additions and 3 deletions

View file

@ -1295,10 +1295,12 @@ impl Engine {
let now = self.now_secs(); let now = self.now_secs();
let key = (channel.to_ascii_lowercase(), target_display.to_ascii_lowercase()); 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 }); 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.ban = ban;
vs.voters.insert(from.to_string()); vs.voters.insert(from.to_string());
let count = vs.voters.len() as u16; let count = vs.voters.len() as u16;

View file

@ -338,6 +338,12 @@ pub async fn run(engine: Shared, cfg: GrpcCfg, outbound: broadcast::Sender<LogEn
Ok(a) => a, Ok(a) => a,
Err(e) => return tracing::error!(%e, bind = %cfg.bind, "bad grpc bind address"), 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 has_tls = cfg.tls.is_some();
let accounts_svc = AccountsService { engine: engine.clone(), token: cfg.token.clone() }; let accounts_svc = AccountsService { engine: engine.clone(), token: cfg.token.clone() };
let stats_svc = StatsService { engine: engine.clone(), token: cfg.token.clone() }; let stats_svc = StatsService { engine: engine.clone(), token: cfg.token.clone() };