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.
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.
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.
Opt-in: middleware is canonical across all MCPs; default-on behaviour change risks every consumer. Gate behind an option.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Migrated here from
gitea-mcp#7—BearerMiddlewaremoved to this module ingitea-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 viaunauthorized(). No throttling, no delay. An attacker can:subtle.ConstantTimeCompareblunts per-compare timing, but nothing rate-limits attempts);cache.Get+ parse is measurably slower than a constant-time byte compare).Proposed change
Retry-Afterheader when the limit trips.Design constraints (shared-lib + deployment reality)
r.RemoteAddris the proxy. Per-IP keying must read a trusted forwarded header — and which header is trusted is deployment-specific. Don't hardcodeX-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.gitea-mcp#6deferred work: the 503 +WWW-Authenticate: error="temporarily_unavailable"item from #6 also touches this same fall-through path /Validateerror classification. Worth designing the failure-path rework once, covering both.Acceptance
Retry-Afteremitted when limitedRef: gitea-mcp#7, gitea-mcp#6