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:
parent
5d67133295
commit
34892dbba7
2 changed files with 11 additions and 3 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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() };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue