Skip to content

operator: rate-limit the diagnostics that fan out to third parties - #665

Merged
MikeyPetrillo merged 1 commit into
mainfrom
claude/sweet-brown-i99jl3
Aug 3, 2026
Merged

operator: rate-limit the diagnostics that fan out to third parties#665
MikeyPetrillo merged 1 commit into
mainfrom
claude/sweet-brown-i99jl3

Conversation

@MikeyPetrillo

Copy link
Copy Markdown
Owner

Closes the last open code-scanning alert (CodeQL js/missing-rate-limiting, high, #81 on /__operator/ledger-sync.json).

Why the existing mitigations were not enough

The route was already operator-authed and already cached its RPC fan-out for 15s, and the alert id had been written into a comment as if that settled it. It does not — these are three different guarantees and only two were present:

  • a cache bounds how often we hit an upstream
  • auth bounds who can call
  • neither bounds how much work one caller can queue against this process

Applied to the class, not the instance

discovery-gap.json reaches the on-chain economy snapshot and has the identical shape. CodeQL flagged only its sibling; both are now bounded.

Budget is deliberately generous (30/min, 300/hour) — these are human-driven diagnostics, and a limit that locks an operator out of their own dashboard mid-incident would be worse than the abuse it prevents. Verified the bound does not spill onto the cheap operator endpoints.

Ordering

The limiter runs after operatorAuthed, which matters: an unauthenticated caller still gets 404 and can never elicit a 429, so the new status is not a route-existence oracle and an attacker cannot poison an operator's bucket.

Verification

  • End-to-end: unauthed 404, authed 200, and a 35-request burst produced exactly 5 × 429 against a 30/min budget
  • 4 assertions added to scripts/test-operator-auth.js (34 total), in CI
  • Mutations killed: removing the limiter from the flagged route, and from the unflagged sibling
  • Independent security review of the diff: no findings — ordering correct, no interpolation of untrusted input, /__operator middleware already sets no-store, private + Vary on the 429 path, and the new bucket shares no table with the credential-guessing limiter
  • Sweeps clean: 209 strict / 295 lenient tools, 526/526 through MCP

Implementation note

The first version called clientIp(req), which does not exist in this file. It passed node --check and would have thrown on the first request. It now derives the IP exactly as the MCP transport limiter does, honouring the app's trust-proxy setting.

Scanning status: 0 open Dependabot alerts, 0 open secret-scanning alerts.

…hird parties

Closes the one open code-scanning alert (CodeQL js/missing-rate-limiting, high,
#81 on /__operator/ledger-sync.json).

The route was already operator-authed and already cached its RPC fan-out for
15s, and someone had written the alert id into a comment as if that settled it.
It does not: a cache bounds how often we hit an UPSTREAM, auth bounds WHO can
call, and neither bounds how much work one caller can queue against this
process. Those are three different guarantees and only two were present.

Applied to BOTH expensive operator routes, not only the flagged one.
discovery-gap.json reaches the on-chain economy snapshot and has the identical
shape; the scanner found one instance and the defect is the class. That
distinction is the recurring theme of today's work.

Budget is deliberately generous (30/min, 300/hour): these are human-driven
diagnostics, and a limit that locks an operator out of their own dashboard
mid-incident would be worse than the abuse it prevents. Verified the bound does
not spill onto the cheap operator endpoints.

4 assertions added to scripts/test-operator-auth.js (34 total). Mutations
killed: removing the limiter from the flagged route, and from the unflagged
sibling.

Note on the implementation: the first version called clientIp(req), which does
not exist in this file. It passed `node --check` and would have thrown on the
first request - the same call-before-definition slip as earlier today, caught
the same way, by running it rather than trusting the syntax check. It now
derives the ip exactly as the MCP transport limiter does, honouring the app's
trust-proxy setting so Railway's proxy is not treated as the client.

Scanning status at this commit: 0 open Dependabot alerts, 0 open secret-scanning
alerts, and this was the last open code-scanning alert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MikeyPetrillo
MikeyPetrillo merged commit b88c87f into main Aug 3, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant