Global rate limits in multi-tenant systems create a noisy-neighbor availability problem: a single tenant with a burst of traffic — legitimate or scripted — exhausts the shared token bucket and causes 429 responses for all other tenants. CWE-770 (Allocation of Resources Without Limits or Throttling) is the relevant defect. While this is a low-severity finding in isolation, it becomes a practical denial-of-service vector in SaaS products with free tiers where abuse is common and the impact falls on paying customers.
Low because the impact is availability degradation for other tenants rather than data exposure, and is bounded by the rate limit window duration.
Partition rate limit buckets by tenant identifier so one tenant's burst cannot exhaust another's capacity. In src/middleware.ts, include the organization ID in the rate limit key:
// Upstash Ratelimit — per-tenant sliding window
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: `ratelimit:org:${session.user.organizationId}`
})
const { success } = await ratelimit.limit(session.user.organizationId)
if (!success) return new Response('Rate limit exceeded', { status: 429 })
For plans with different quotas (free vs. paid), vary the slidingWindow limit based on the tenant's subscription tier retrieved from session claims, not from a per-request DB lookup.
ID: saas-multi-tenancy.tenant-boundaries.rate-limiting-per-tenant
Severity: low
What to look for: Examine rate limiting configuration — middleware, API route-level rate limits, Upstash Ratelimit setup, nginx/Vercel rate limit rules. Check whether rate limits are applied per user, per tenant, or globally. In a multi-tenant system, a single tenant with high usage should not be able to exhaust the rate limit budget for all other tenants.
Pass criteria: List all rate-limited endpoints. Rate limiting is applied at the tenant level with at least 1 distinct limit per tenant (by organization ID or team ID) rather than globally across all tenants, or uses a combination of per-user and per-tenant limits. High-usage by one tenant does not reduce available capacity for other tenants.
Fail criteria: Rate limits are applied globally with no per-tenant component, meaning one tenant's burst usage could cause 429 responses for all other tenants.
Skip (N/A) when: No rate limiting is implemented at all (this is noted but the absence itself is covered by the API Design audit). Skip this specific check only when no rate limiting middleware or configuration of any kind is detected. Signal: no rate-limit middleware, no Upstash Ratelimit, no X-RateLimit header configuration, no rate limiting library in dependencies.
Detail on fail: Describe the current rate limit scope. Example: "Rate limiting in src/middleware.ts uses a global Redis key 'api-rate-limit' with no tenant component. All tenants share one token bucket."
Remediation: Use tenant-scoped rate limit keys:
// Upstash Ratelimit example
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: `ratelimit:org:${session.user.organizationId}` // per-tenant bucket
})