Skip to content

[BUG] Denial of Service (DoS) Vulnerability and Rate Limit Bypass in Badge Rate Limiter #3120

Description

@basantnema31

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:

  1. 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.

  1. 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

  • badge-rate-limit.ts

Steps to Reproduce

  1. Send requests to /api/badge/commits?user=someuser with random IP addresses in the X-Forwarded-For header.
  2. Observe that the rate limit never triggers because every request appears to come from a new IP.
  3. 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

  1. 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.
  2. 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

Metadata

Metadata

Labels

gssoc:assignedGSSoC: Issue assigned to a contributor

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions