Rate Limiting Bypass via X-Forwarded-For Header Spoofing, How to Block It
If your rate limiter keys off the X-Forwarded-For header without validating where that header actually came from, an attacker can trivially bypass rate limiting by simply sending their own fabricated value in that header on every request — since X-Forwarded-For is client-settable by default, blindly trusting it defeats the entire purpose of IP-based rate limiting.
The Problem
A rate limiter configured to key off X-Forwarded-For (commonly done because a reverse proxy or load balancer sits in front of the application) appears to have no real effect against a determined attacker — they can send an unlimited number of requests by simply varying the header value on each one:
curl -H "X-Forwarded-For: 1.1.1.1" https://api.example.com/login
curl -H "X-Forwarded-For: 2.2.2.2" https://api.example.com/login
curl -H "X-Forwarded-For: 3.3.3.3" https://api.example.com/login
# each request appears to come from a "different" client to a naive rate limiter
Why It Happens
X-Forwarded-For is just a regular HTTP header — unless something strips or validates it, any client can set it to literally any value they want, including a value specifically crafted to make each of their requests appear to originate from a different, unrelated IP address. The header exists to let a trusted proxy tell backend services what the original client's real IP was, but it only serves that purpose safely when the receiving application knows to only trust this header when it's genuinely added by a proxy it controls, and to actively discard or ignore any value a client tried to set directly.
The Fix
Configure your web server or application to only trust X-Forwarded-For when the request genuinely originates from a proxy you control, and to use only the specific portion of the header your trusted proxy actually added — not blindly trusting the entire header value as sent, since a malicious client can prepend their own fake values to a header your proxy also appends to.
For Nginx sitting in front of your application, using the ngx_http_realip_module correctly restricts which upstream sources are trusted to supply this header at all:
set_real_ip_from 10.0.0.0/8; # only trust this header from your own internal network/proxy
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from restricts trust to requests genuinely originating from your specified trusted proxy IP ranges — a request arriving directly from the public internet, even with a crafted X-Forwarded-For header attempting to spoof a different origin, is not treated as coming from within this trusted range, and the spoofed header value is correctly ignored for real-IP determination purposes.
At the application level, if you're parsing X-Forwarded-For directly rather than relying entirely on a reverse proxy to resolve it first, understand that the header can contain a comma-separated chain of IPs (each proxy in a chain typically appends its own value) — the correct real client IP is generally the leftmost value in that chain, but only if every proxy in the chain between the client and your application is genuinely trusted to have added its own entry correctly, not simply passed through whatever the client originally sent:
# Naive and exploitable - trusts whatever the client claims
const clientIp = req.headers['x-forwarded-for'];
// Better - use a library specifically designed to correctly parse trusted proxy chains
const clientIp = req.ip; // when using Express with 'trust proxy' correctly configured
In Express specifically, configure trust proxy with the actual number of trusted proxy hops (or specific trusted IP ranges), rather than setting it to blindly trust everything, which is itself a common misconfiguration that reintroduces the exact same spoofing vulnerability:
// Trusts exactly one hop - appropriate if you have exactly one reverse proxy in front
app.set('trust proxy', 1);
// Or, more precisely, trust specific known proxy IP ranges
app.set('trust proxy', ['10.0.0.0/8']);
Setting trust proxy to true (trusting everything unconditionally) is a common but genuinely dangerous misconfiguration for exactly this reason — it tells Express to trust the entire X-Forwarded-For chain as provided, including any portion of it a malicious client might have fabricated before it ever reached your actual trusted proxy.
Still Not Working?
If you've correctly configured trusted proxy handling but rate limiting still seems bypassable, consider layering additional rate limiting dimensions beyond IP address alone — combining IP-based limiting with per-account or per-API-key limiting (for authenticated endpoints) provides a second, independent layer that isn't defeated purely by IP rotation or spoofing, since an attacker attempting to bypass limits by varying their apparent source IP still can't avoid a limit tied to their actual authenticated identity or API credential, which is under your control and impossible to spoof through a client-supplied header.