How proxies preserve client information
When a request passes through reverse proxies and load balancers, the backend
sees the proxy’s address, not the client’s. Forwarding headers carry the
original client IP, protocol and host along the chain. This reference covers the
standardised Forwarded header and the legacy X-Forwarded-* set, plus how to
parse a proxy chain safely.
Legacy vs standard headers
| Purpose | Legacy header | Standard (RFC 7239) |
|---|---|---|
| Client IP chain | X-Forwarded-For: a, b, c | Forwarded: for=a, for=b |
| Original scheme | X-Forwarded-Proto: https | Forwarded: proto=https |
| Original hostname | X-Forwarded-Host: example.com | Forwarded: host=example.com |
| Receiving proxy identity | (no standard legacy equivalent) | Forwarded: by=proxy.example |
The legacy set is more widely supported by existing middleware and CDNs. The RFC 7239 Forwarded header is the formal standard and carries more information in a single, unambiguous field. Many modern proxies and frameworks support both.
How it works
Each proxy appends what it observed. The legacy headers split the data across
three fields; the standard Forwarded header combines them:
X-Forwarded-For: 203.0.113.7, 198.51.100.2, 10.0.0.1
X-Forwarded-Proto: https
X-Forwarded-Host: example.com
Forwarded: for=203.0.113.7;proto=https;host=example.com, for=198.51.100.2
The list grows left-to-right as hops are traversed, so the leftmost
X-Forwarded-For entry is the original client as reported — but any client
can forge entries. Trust only the addresses appended by proxies you control:
count back from the right by your number of trusted hops to find the true
client IP.
Extracting the real client IP safely
The naive approach — reading the leftmost X-Forwarded-For value — is exploitable. An attacker can send a request with a crafted X-Forwarded-For header to spoof an arbitrary IP. For example:
X-Forwarded-For: 1.2.3.4, 203.0.113.7
If your CDN or load balancer appends the real client address on the right and you trust the leftmost entry, an attacker who sends X-Forwarded-For: 1.2.3.4 in their request will appear to come from 1.2.3.4.
The correct approach: configure your framework with the number of trusted reverse proxies in your infrastructure. From the rightmost address in the chain, count back by that number. The address at that position is the real client IP — everything to its left was supplied by the client or untrusted intermediaries.
Most frameworks expose this directly: Express (trust proxy: 1), Django (NUM_PROXIES), Laravel (TrustProxies middleware), etc. Set the count to match your actual infrastructure and let the framework do the math.
Tips and notes
- Configure a trusted-proxy count in your framework; never trust the leftmost value unconditionally.
- IPv6 in
Forwardedmust be quoted and bracketed:for="[2001:db8::1]". X-Forwarded-Protolets a TLS-terminating proxy tell the app the original scheme washttps, so redirect logic andSecurecookie checks work correctly behind a proxy.- Prefer
Forwardedfor new systems; keepX-Forwarded-*for compatibility with existing tooling. - RFC 7239 allows obfuscated identifiers (
for=_hidden) and the special tokenunknownwhen the address cannot be disclosed.