Description
The application implements a custom sliding window rate limiter for badge generation endpoints in src/lib/badge-rate-limit.ts.
Two critical vulnerabilities exist in this implementation:
- Denial of Service (Algorithmic Complexity Attack)
The pruneStore(now) function is called on every rate limit check to clean up expired entries from the store map if the size exceeds 500:
function pruneStore(now: number): void {
if (store.size < 500) return;
const cutoff = now - WINDOW_MS;
for (const [key, entry] of store) {
if (entry.windowStart < cutoff) store.delete(key);
}
}
If an attacker sends requests from thousands of unique IPs within the 60-second window (WINDOW_MS), the map size will grow large (e.g. 50,000 entries). Because none of these entries are older than 60 seconds, pruneStore will not delete any of them. However, it will still loop through all 50,000 entries of the map on every single incoming request. This causes CPU spikes and application unresponsive/DoS.
- Rate Limit Bypass via Client-Controlled IP Headers
The getBadgeClientIp function extracts the client IP address by reading the X-Forwarded-For header:
export function getBadgeClientIp(req: NextRequest): string {
return (
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
...
);
}
Because the application trusts X-Forwarded-For blindly without verifying whether the request comes from a trusted reverse proxy (such as Cloudflare or Vercel), any user can spoof this header (e.g. X-Forwarded-For: 1.2.3.4) to bypass the rate limiter completely.
Affected Files
Steps to Reproduce
- Send requests to
/api/badge/commits?user=someuser with random IP addresses in the X-Forwarded-For header.
- Observe that the rate limit never triggers because every request appears to come from a new IP.
- Observe that after 500+ unique IPs are registered, the server's CPU usage increases as
pruneStore loops through the entire map on every single request.
Suggested Solution
- Avoid looping through the entire map in
pruneStore. Use a TTL-based cache library (such as lru-cache) or a cron-based/debounced cleanup that runs once every minute instead of on every request.
- Do not trust
X-Forwarded-For directly unless it is verified against a trusted reverse proxy IP range, or fallback to native request IPs.
Labels: gssoc, quality:exceptional, level:critical
/assign
Description
The application implements a custom sliding window rate limiter for badge generation endpoints in
src/lib/badge-rate-limit.ts.Two critical vulnerabilities exist in this implementation:
The
pruneStore(now)function is called on every rate limit check to clean up expired entries from thestoremap if the size exceeds 500:If an attacker sends requests from thousands of unique IPs within the 60-second window (
WINDOW_MS), the map size will grow large (e.g. 50,000 entries). Because none of these entries are older than 60 seconds,pruneStorewill not delete any of them. However, it will still loop through all 50,000 entries of the map on every single incoming request. This causes CPU spikes and application unresponsive/DoS.The
getBadgeClientIpfunction extracts the client IP address by reading theX-Forwarded-Forheader:Because the application trusts
X-Forwarded-Forblindly without verifying whether the request comes from a trusted reverse proxy (such as Cloudflare or Vercel), any user can spoof this header (e.g.X-Forwarded-For: 1.2.3.4) to bypass the rate limiter completely.Affected Files
Steps to Reproduce
/api/badge/commits?user=someuserwith random IP addresses in theX-Forwarded-Forheader.pruneStoreloops through the entire map on every single request.Suggested Solution
pruneStore. Use a TTL-based cache library (such aslru-cache) or a cron-based/debounced cleanup that runs once every minute instead of on every request.X-Forwarded-Fordirectly unless it is verified against a trusted reverse proxy IP range, or fallback to native request IPs.Labels: gssoc, quality:exceptional, level:critical
/assign