Request header · non-standard
> X-Forwarded-For: 203.0.113.7, 198.51.100.2X-Forwarded-For
X-Forwarded-For carries the client IP through proxies and load balancers, but clients can forge it. Trust it safely in nginx, Express, Cloudflare and AWS.
- Direction
- Request
- Category
- Proxies
- Spec
- MDN reference
- Status
- Non-standard: De-facto convention; the standard form is Forwarded
- JS can set it
- Yes
- CORS-safelisted
- No: may trigger a preflight
On this page
TL;DR:
X-Forwarded-Forlists the client address followed by each proxy hop, but any client can forge it. Trust only the entries added by proxies you operate: configure the trusted proxies (nginxset_real_ip_from, Expresstrust proxy) instead of reading the leftmost value.
What is X-Forwarded-For?
The X-Forwarded-For (XFF) header records the address a request came from before it reached your server, because the TCP connection your server sees belongs to the nearest proxy. Each proxy appends the address of the peer it received the request from.
X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178
Read left to right, that is: client 203.0.113.195, which went through proxy 70.41.3.18, whose address was seen by the last proxy 150.172.238.178. The last proxy’s own address is the socket peer and is not in the header. It is not standardised (MDN lists it as non-standard); Forwarded is the RFC 7239 replacement.
Why you cannot trust it by default
A proxy appends; it does not replace what the client sent. So a client that sends this directly:
curl -H 'X-Forwarded-For: 1.2.3.4' https://example.com/
reaches your app, behind one proxy, as X-Forwarded-For: 1.2.3.4, <real client IP>. The forged 1.2.3.4 is leftmost. Code that takes the first value now rate-limits, geo-blocks and audit-logs the wrong address, and an attacker can pick any IP to dodge a per-IP limit or to impersonate an allowlisted one.
Rule: start at the socket peer. If it is one of your proxies, step left one entry at a time while each entry is also one of your proxies. The first entry that is not yours is the client. Everything left of it is attacker-controlled text.
If no proxy sits in front of your origin, ignore the header entirely and use the socket address. If your origin is reachable directly (bypassing the CDN), every header is spoofable; lock the origin down to the proxy’s IP ranges.
Configure trusted proxies
nginx (ngx_http_realip_module, needs --with-http_realip_module, present in common distro builds). Without real_ip_recursive on, nginx takes the last address in the header; with it, nginx skips trusted addresses from the right:
set_real_ip_from 10.0.0.0/8; # your load balancer / proxy ranges
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
$remote_addr then holds the client address and $realip_remote_addr keeps the original peer. Log both while you debug.
Express. Count hops or name subnets; never use true on an internet-facing app, because it trusts the leftmost value:
// One reverse proxy (for example nginx or a single ALB) in front of Node
app.set('trust proxy', 1)
// Or name the proxy ranges
app.set('trust proxy', 'loopback, 10.0.0.0/8')
app.get('/ip', (req, res) => res.json({ ip: req.ip, chain: req.ips }))
With trust proxy unset, req.ip is the socket address, i.e. the proxy. The setting also controls req.protocol and req.hostname, so a wrong value affects secure-cookie and redirect behaviour as well.
Cloudflare. Cloudflare adds CF-Connecting-IP containing the single client address it saw, and appends to X-Forwarded-For. Use CF-Connecting-IP, but only if requests can reach you only through Cloudflare; otherwise a client can send that header too. Enterprise plans can also add True-Client-IP.
AWS ALB. The Application Load Balancer appends the connecting address to X-Forwarded-For by default, so the client-supplied part stays on the left. The ALB attribute routing.http.xff_header_processing.mode can be set to append (default), preserve or remove. CloudFront also appends the viewer IP, so behind CloudFront and an ALB there are two proxy hops to trust.
Kubernetes ingress-nginx. Controls live in the controller ConfigMap: use-forwarded-headers (trust inbound XFF, only when something you control is in front) and proxy-real-ip-cidr (which sources are trusted). With a cloud load balancer, externalTrafficPolicy: Local on the Service preserves the client source address.
Django. Django does not parse X-Forwarded-For for you: REMOTE_ADDR is the proxy. Either have the proxy layer rewrite it (nginx realip) or use a library that takes a trusted-proxy count, and never read request.META['HTTP_X_FORWARDED_FOR'] raw.
Logging and gotchas
- Log the full header plus the socket address. The socket address is the only value the client cannot forge.
- The header can contain IPv6 addresses, ports in some proxies (
1.2.3.4:5678), and the stringunknown. Parse and validate before using it as a key. - Values are comma separated and the header may appear as several separate header lines; treat them as one comma-joined list.
- Do not use XFF alone for authorization. An IP allowlist that trusts a header you did not verify is a bypass.
Related Headers
- Forwarded - the standard alternative
- X-Forwarded-Proto - original scheme
- Via - proxies in the path
- Host
Frequently asked questions
What is X-Forwarded-For?
A de facto request header that each proxy or load balancer appends the connecting peer address to, so the origin can see the chain of addresses behind the TCP connection. It is not an IETF standard; the standardised equivalent is Forwarded (RFC 7239).
How do I get the real client IP from X-Forwarded-For?
Start from the socket address, and only if it belongs to one of your own proxies, read the header from the right, skipping addresses that belong to your own proxies. The first address you do not control is the client. Reading the leftmost value is wrong unless every hop in front of you strips incoming copies of the header.
Can X-Forwarded-For be spoofed?
Yes. Any client can send X-Forwarded-For: 1.2.3.4 and a proxy that appends to the header will keep the forged value at the left. Only the addresses added by proxies you operate are trustworthy, which is why trusted-proxy configuration exists in nginx, Express and similar tools.
Should I use X-Forwarded-For or Forwarded?
Use whichever your proxies actually emit. X-Forwarded-For is far more widely produced by CDNs, ALBs and ingress controllers, while Forwarded is the standard but is rarely generated by default. Never mix them without deciding which one is authoritative.
Why does my app see the load balancer IP instead of the client IP?
The TCP peer is the proxy, so remoteAddr, REMOTE_ADDR or req.ip reports the proxy until the framework is told which proxies to trust. Enable trust proxy in Express, set_real_ip_from and real_ip_header in nginx, or read the CDN-specific header such as CF-Connecting-IP.
Sources
- MDN Web Docs: X-Forwarded-Fordeveloper.mozilla.org
- RFC 7239: Forwarded HTTP Extensionrfc-editor.org
- nginx: ngx_http_realip_modulenginx.org
- Express: behind proxies (trust proxy)expressjs.com
- Cloudflare: HTTP request headers (CF-Connecting-IP)developers.cloudflare.com
- AWS: HTTP headers and Application Load Balancers (X-Forwarded-For)docs.aws.amazon.com
Related
Reverse Proxy
A reverse proxy sits in front of origin servers and forwards client requests to them. Learn the headers it adds, nginx proxy_pass pitfalls and 502/504 causes.
Forwarded
Learn how the Forwarded header preserves original client information (IP, protocol, host) that would otherwise be lost when requests pass through proxies.
Accept Header
Learn how the Accept header tells servers which content types (JSON, HTML, XML) your client can handle. Master content negotiation and quality values.
Accept-Language Header
Learn how the Accept-Language header tells servers which languages your client prefers for localized content. Understand language tags and quality values.