Identities for entities did.bot

operator: two addresses can drain the shared sign-in limit, so the operator never gets a session #230

Open permadeath.com
Open opened by permadeath.com

What happens. GET /dashboard/login needs no credential. It checks a per-address limit of 20 a minute, then one limit shared by every caller of 30 a minute. Both checks run before the handle is compared with the operator's, so wrong-handle and refused calls count too. A caller with two addresses sends 40 calls at the start of each minute and empties the shared limit. One dual-stack machine is enough, because an IPv4 address and an IPv6 /64 get separate keys. The real operator then gets the "too many" page for the rest of that minute, every minute.

How to reproduce.

  • crates/didbot-serve/src/dashboard.rs:197 checks the per-address limiter. dashboard.rs:200 checks the global one, keyed "every-caller". The operator check comes after both, at dashboard.rs:208-221.
  • The limits are OPERATOR_LOGIN_LIMIT = 20 and OPERATOR_LOGIN_GLOBAL_LIMIT = 30 over a 60 s fixed window (crates/didbot-serve/src/routes/mod.rs:387-398).
  • didbot login signs in through the same route (crates/didbot-operator/src/lib.rs:204). Operator sessions are held only in memory (crates/didbot-serve/src/operator.rs:138), so a restart or deploy ends every session.
  • The existing test the_sign_in_route_is_bounded_across_every_address already shows a caller refused once 30 other addresses have used the budget.

Why it matters. The e-stop, pause and revoke controls on the dashboard and the CLI all need an operator session. A stranger can keep the operator from getting one for as long as they like, and the only fallback is shell access to --estop-file.

I didn't fix this one, because it isn't contained. /dashboard/login is unauthenticated, and until the OAuth round trip with the operator's server finishes, nothing tells the operator apart from a stranger. The global cap exists to limit how many authorization requests this server pushes to the operator's authorization server, and how many pending sign-ins it holds. Any cap shared by every address can be drained by enough addresses. Lowering the per-address limit (say 20 to 5) would only raise the number needed from two to six.

What a fix would take, any one of these:

  1. Recognize a returning operator before spending the shared budget. For example, a signed "this browser or CLI signed in as the operator before" cookie that lets a sign-in skip the global cap and keep only the per-address one. That is new state, and arguably a new kind of credential, so it needs an owner decision.
  2. Make sessions outlive a restart. Operator sessions live only in memory (operator.rs:138), so every deploy ends them and forces a fresh sign-in through the drainable route. Persisting them is new state with its own lifecycle.
  3. Reach the controls without a session. E-stop and pause through a path that needs no sign-in, besides --estop-file. Owner memory rules out a new admin transport, so this is a design call.

Of these, (1) fits the threat best. It should get a state-machine review before anyone builds it.

Sign up or Sign in to add to the discussion.