Without per-user rate limiting, a single authenticated account can exhaust compute, database connections, and third-party API quotas for all users. OWASP API Security Top 10 2023 API4 (Unrestricted Resource Consumption) identifies per-user rate limiting as the primary control against API abuse. CWE-770 (Allocation of Resources Without Limits) covers the structural gap. Beyond resource exhaustion, unthrottled authenticated endpoints expose LLM token costs, external API call charges, and email sending quotas to abuse by any valid user. Competitors, scrapers, and compromised accounts can all trigger cost events at scale without rate limiting.
Medium because unlimited authenticated requests enable resource exhaustion and cost amplification attacks that degrade service for all users without requiring new credentials.
Key your rate limiter on user.id, not IP address, so that authenticated users can't bypass limits by rotating IPs. Return a Retry-After header so well-behaved clients back off correctly.
// src/lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1m'), // 100 req/min per user
})
export async function checkRateLimit(userId: string) {
const { success, reset } = await ratelimit.limit(`user:${userId}`)
if (!success) {
throw Object.assign(new Error('Rate limit exceeded'), {
status: 429,
retryAfter: Math.ceil((reset - Date.now()) / 1000)
})
}
}
Call checkRateLimit(user.id) at the top of any route handler before executing business logic.
ID: api-security.abuse-prevention.rate-limiting-auth
Severity: medium
What to look for: Enumerate every relevant item. Check middleware or route handlers for rate limiting logic. Verify that authenticated requests are rate-limited per user (e.g., 100 requests per minute per user ID). Check that requests over the limit return 429 Too Many Requests with a Retry-After header.
Pass criteria: At least 1 of the following conditions is met. Rate limiting is enforced on API endpoints, tracking usage per authenticated user. Exceeded requests return 429 with Retry-After header. Limits are reasonable for the API's intended use case.
Fail criteria: No rate limiting is implemented on API endpoints, or rate limits are not enforced consistently.
Skip (N/A) when: The API is internal-only or explicitly designed to have no rate limits.
Detail on fail: "No rate limiting on endpoints — users can make unlimited requests" or "Rate limiting not tracked per user — rate limit is global for all users"
Remediation: Implement per-user rate limiting using a library like express-rate-limit:
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
keyGenerator: (req) => req.user.id, // Rate limit per user
handler: (req, res) => {
res.status(429).json({ error: 'Too many requests' })
}
})
app.use('/api', limiter)