auth: no rate limiting / timing defence on Bearer auth failures #2

Open
opened 2026-06-02 14:12:46 +00:00 by mathias · 0 comments
Owner

Migrated here from gitea-mcp#7BearerMiddleware moved to this module in gitea-mcp@658f4ba. The middleware is shared, so any fix lands for every consumer at once.

Problem

BearerMiddleware (auth/bearer.go) rejects on the fall-through path with an immediate 401 via unauthorized(). No throttling, no delay. An attacker can:

  • enumerate the static token at full request rate (the subtle.ConstantTimeCompare blunts per-compare timing, but nothing rate-limits attempts);
  • probe for coarse timing differences between the static-compare path and the JWT-validate path (JWKS cache.Get + parse is measurably slower than a constant-time byte compare).

Proposed change

  • Per-IP failure rate limiting (e.g. 10 failures/min, exponential backoff) on the fall-through path.
  • Retry-After header when the limit trips.
  • Optional short fixed sleep on the failure path to blunt path-timing enumeration.

Design constraints (shared-lib + deployment reality)

  1. Client IP: every consumer sits behind oauth2-proxy / k3s ingress, so r.RemoteAddr is the proxy. Per-IP keying must read a trusted forwarded header — and which header is trusted is deployment-specific. Don't hardcode X-Forwarded-For; make the IP-extraction strategy injectable, default to a safe no-op (limit disabled) so a misconfigured consumer fails open on availability, not closed on a spoofable header.
  2. State + memory: a per-IP counter map grows unbounded under a spoofed-IP flood — the rate limiter becomes its own DoS vector. Needs bounded/TTL eviction.
  3. Opt-in: middleware is canonical across all MCPs; default-on behaviour change risks every consumer. Gate behind an option.
  4. Overlaps gitea-mcp#6 deferred work: the 503 + WWW-Authenticate: error="temporarily_unavailable" item from #6 also touches this same fall-through path / Validate error classification. Worth designing the failure-path rework once, covering both.

Acceptance

  • Opt-in per-IP failure rate limiting with injectable IP-extraction + bounded state
  • Retry-After emitted when limited
  • Failure-path timing defence (fixed sleep or equivalent), documented
  • Tests: limit trips after N failures; state is bounded; disabled-by-default path unchanged

Ref: gitea-mcp#7, gitea-mcp#6

Migrated here from `gitea-mcp#7` — `BearerMiddleware` moved to this module in `gitea-mcp@658f4ba`. The middleware is shared, so any fix lands for every consumer at once. ## Problem `BearerMiddleware` (`auth/bearer.go`) rejects on the fall-through path with an immediate 401 via `unauthorized()`. No throttling, no delay. An attacker can: - enumerate the static token at full request rate (the `subtle.ConstantTimeCompare` blunts per-compare timing, but nothing rate-limits attempts); - probe for coarse timing differences between the static-compare path and the JWT-validate path (JWKS `cache.Get` + parse is measurably slower than a constant-time byte compare). ## Proposed change - Per-IP failure rate limiting (e.g. 10 failures/min, exponential backoff) on the fall-through path. - `Retry-After` header when the limit trips. - Optional short fixed sleep on the failure path to blunt path-timing enumeration. ## Design constraints (shared-lib + deployment reality) 1. **Client IP**: every consumer sits behind oauth2-proxy / k3s ingress, so `r.RemoteAddr` is the proxy. Per-IP keying must read a trusted forwarded header — and which header is trusted is deployment-specific. Don't hardcode `X-Forwarded-For`; make the IP-extraction strategy injectable, default to a safe no-op (limit disabled) so a misconfigured consumer fails open on availability, not closed on a spoofable header. 2. **State + memory**: a per-IP counter map grows unbounded under a spoofed-IP flood — the rate limiter becomes its own DoS vector. Needs bounded/TTL eviction. 3. **Opt-in**: middleware is canonical across all MCPs; default-on behaviour change risks every consumer. Gate behind an option. 4. **Overlaps `gitea-mcp#6` deferred work**: the 503 + `WWW-Authenticate: error="temporarily_unavailable"` item from #6 also touches this same fall-through path / `Validate` error classification. Worth designing the failure-path rework once, covering both. ## Acceptance - [ ] Opt-in per-IP failure rate limiting with injectable IP-extraction + bounded state - [ ] `Retry-After` emitted when limited - [ ] Failure-path timing defence (fixed sleep or equivalent), documented - [ ] Tests: limit trips after N failures; state is bounded; disabled-by-default path unchanged Ref: gitea-mcp#7, gitea-mcp#6
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/mcp-chassis#2