Skip to content

security: rate limiter falls back to client-controlled X-Forwarded-For despite comment saying it MUST NOT be trusted #3721

Description

@tg12

Location

server/_shared/rate-limit.ts, lines 24–28:

// x-forwarded-for is client-settable and MUST NOT be trusted for rate limiting.
return (
  request.headers.get('cf-connecting-ip') ||
  request.headers.get('x-real-ip') ||
  request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
  '0.0.0.0'
);

Impact

The code contains its own proof of the bug in the comment immediately above it. X-Forwarded-For is entirely client-controlled — any caller can set it to an arbitrary IP. When cf-connecting-ip and x-real-ip are absent (direct connections, non-Cloudflare environments, local dev), the rate limiter keys on a value the client chose. An attacker can:

  1. Exhaust the rate limit budget for a legitimate user's IP by spoofing it.
  2. Bypass their own rate limit by cycling through synthetic IPs in X-Forwarded-For.

The global limit is Ratelimit.slidingWindow(600, '60 s') — bypassing it enables unlimited API calls including the premium-gated /api/chat-analyst endpoint.

Fix

Remove the x-forwarded-for fallback entirely. If neither cf-connecting-ip nor x-real-ip is set, use '0.0.0.0' (or skip rate limiting) rather than trusting a client-supplied header:

return (
  request.headers.get('cf-connecting-ip') ||
  request.headers.get('x-real-ip') ||
  '0.0.0.0'
);

If x-real-ip is also proxied and potentially spoofable in some deploy configurations, document which header is authoritative and enforce that only in production.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions