Skip to content

[Bug Report] CubeAPI rate limiter is bypassable with an unvalidated X-API-Key, and lumps all Bearer clients into one bucket #1379

Description

@dwin-gharibi

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:

  1. Bypass — a valid Bearer token plus a random X-API-Key per request means governor allocates a
    fresh full-quota bucket every time.
  2. 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.
  3. Unbounded memoryRateLimiter::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.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions