The rate limiter buckets on the raw X-API-Key header. When a client authenticates with
Authorization: Bearer, that header is never validated by the auth middleware but is still used as the
limiter key — so rotating it gives a fresh full-quota bucket per request. Clients that send no X-API-Key
all share one "anonymous" bucket, and the keyed state store is never reclaimed.
Environment
- CubeSandbox version / commit:
5960c56 (master)
- Host OS and kernel version: any
- KVM info (
modinfo kvm): n/a
- Deployment mode: single-node / cluster
- Relevant component: CubeAPI
Steps to Reproduce
Run CubeAPI with CUBE_API_KEY=supersecret --rate-limit-per-sec 3, then send 30 requests each way:
curl -H "Authorization: Bearer supersecret" http://127.0.0.1:3000/sandboxes
curl -H "Authorization: Bearer supersecret" -H "X-API-Key: rotating-$i" http://127.0.0.1:3000/sandboxes
Expected Behavior
Both are limited after 3 requests/second.
Actual Behavior
A) Bearer only (shared 'anonymous' bucket, limit=3/s) : 30 requests -> 429=26 non429=4
B) Bearer + rotating unvalidated X-API-Key : 30 requests -> 429=0 non429=30
Additional Context
CubeAPI/src/middleware/rate_limit.rs:16-35:
let key = request.headers()
.get("X-API-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("anonymous") // there is no IP fallback, despite the doc comment
.to_string();
extract_credential (middleware/auth.rs:25-45) prefers Bearer, so with a Bearer token the
X-API-Key header is never validated — yet it still decides the bucket.
Three consequences:
- Bypass — a valid Bearer token plus a random
X-API-Key per request means governor allocates a
fresh full-quota bucket every time.
- Cross-tenant DoS — every Bearer client with no
X-API-Key shares "anonymous". In callback mode,
which is the multi-tenant mode, the limiter degenerates into one global bucket.
- Unbounded memory —
RateLimiter::keyed uses a DashMap and nothing calls retain_recent
(grep -rn 'retain_recent|shrink_to_fit' CubeAPI/src/ is empty), so the bypass in (1) is also a
memory-exhaustion vector.
The doc comment claims a fallback to IP that does not exist, so a reviewer would believe per-IP limiting
is in place.
The rate limiter buckets on the raw
X-API-Keyheader. When a client authenticates withAuthorization: Bearer, that header is never validated by the auth middleware but is still used as thelimiter key — so rotating it gives a fresh full-quota bucket per request. Clients that send no
X-API-Keyall share one
"anonymous"bucket, and the keyed state store is never reclaimed.Environment
5960c56(master)modinfo kvm): n/aSteps to Reproduce
Run CubeAPI with
CUBE_API_KEY=supersecret --rate-limit-per-sec 3, then send 30 requests each way:Expected Behavior
Both are limited after 3 requests/second.
Actual Behavior
Additional Context
CubeAPI/src/middleware/rate_limit.rs:16-35:extract_credential(middleware/auth.rs:25-45) prefers Bearer, so with a Bearer token theX-API-Keyheader is never validated — yet it still decides the bucket.Three consequences:
X-API-Keyper request meansgovernorallocates afresh full-quota bucket every time.
X-API-Keyshares"anonymous". In callback mode,which is the multi-tenant mode, the limiter degenerates into one global bucket.
RateLimiter::keyeduses a DashMap and nothing callsretain_recent(
grep -rn 'retain_recent|shrink_to_fit' CubeAPI/src/is empty), so the bypass in (1) is also amemory-exhaustion vector.
The doc comment claims a fallback to IP that does not exist, so a reviewer would believe per-IP limiting
is in place.