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:197checks the per-address limiter.dashboard.rs:200checks the global one, keyed"every-caller". The operator check comes after both, atdashboard.rs:208-221.- The limits are
OPERATOR_LOGIN_LIMIT = 20andOPERATOR_LOGIN_GLOBAL_LIMIT = 30over a 60 s fixed window (crates/didbot-serve/src/routes/mod.rs:387-398). didbot loginsigns 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_addressalready 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/loginis 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:
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.--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.