X-Forwarded-For (XFF)

Networking

A de-facto HTTP request header that carries the originating client IP plus the IPs of earlier proxies as a comma-separated list, so a server behind proxies can still identify the visitor. It is not a standard and anyone can spoof it: only entries a trusted proxy added are reliable.

Also known as XFF.

9 min read Updated Aug 30, 2026

Full Explanation

X-Forwarded-For (XFF) is a de-facto HTTP request header. It discloses the originating client IP address of a request. It also lists the addresses of the proxies the request passed through earlier, as a comma-separated list: X-Forwarded-For: client, proxy1, proxy2. A proxy makes a request appear to originate from the proxy itself, so the server behind it sees only the address of its immediate peer. That is why XFF exists. XFF is a convention, not a standard. It is not an authentication mechanism. Any client and any proxy can write or rewrite the value. So nothing in it is proof of who connected. It is a request header only. It never contains the address of the hop that is talking to you. That is because you already have that address from the connection itself. The standardized alternative is the Forwarded header of RFC 7239. It is much less frequently used. The working rule for everything below is this: read the list from the right. Walk past the proxies you control. Treat everything further left as attacker-controlled.

How it works

A forward proxy, a reverse proxy or a load balancer terminates the client connection. It then opens its own connection onward. At the transport layer, the next server therefore sees the proxy's address. If a client connection passes through any forward or reverse proxies, the server only sees the final proxy's IP address. That address is often of little use. XFF puts the lost addresses back into the HTTP request.

  1. The client connects to the first proxy. That proxy received the connection from the client. It appends the client's address to X-Forwarded-For.
  2. Each further proxy appends the address of its own immediate sender. So the list grows to the right. The originating client comes first, then each intermediate proxy in order.
  3. A proxy does not add its own address. The last proxy's address is readily available to the server as the remote address at the transport layer. So the header stops one hop short of the reader.
  4. With well-behaved clients and proxies, the leftmost value is the address of the originating client. The rightmost value is the address of the most recent proxy.

Here is the same chain, one hop at a time:

// Client (203.0.113.50) sends request to CDN
GET /api/users HTTP/1.1
Host: api.example.com

// CDN (198.51.100.1) forwards to load balancer, adds client IP
GET /api/users HTTP/1.1
Host: api.example.com
X-Forwarded-For: 203.0.113.50

// Load balancer (10.0.0.1) forwards to app, adds CDN IP
GET /api/users HTTP/1.1
Host: api.example.com
X-Forwarded-For: 203.0.113.50, 198.51.100.1

// App sees: client=203.0.113.50, CDN=198.51.100.1, LB=connection IP

Reading it safely is the hard part, because only the entries your own infrastructure added can be trusted. Search from the rightmost end. Stop at the first address your proxies did not write. That address is the best available estimate of the client. Two configurations implement this:

  • Trusted proxy list: configure the IPs or ranges of your trusted reverse proxies. The list is searched from the rightmost. The search skips all addresses on the trusted list. The first non-matching address is the target address.
  • Trusted proxy count: configure how many reverse proxies sit between the internet and the server. Then search from the rightmost by that count minus one. With one reverse proxy, that proxy added the client's address, so the rightmost entry is the client. With three proxies, the last two entries are internal.

Anything to the left of the address you select is untrusted and may be entirely invented. The first trustworthy value may itself belong to an untrusted intermediate proxy rather than the actual client. But it is the only value suitable for identifying a client for security purposes.

Why it matters for a CDN

A CDN is a reverse proxy in front of the origin. Once it is enabled, the origin's peer is an edge server. Every visitor appears to arrive from the same small set of addresses. The edge itself does not need XFF, since it holds the client connection. Everything behind the edge does. The main uses of client-IP information are diagnostics (event logging, troubleshooting, statistics gathering), access control and abuse management. On a CDN, that extends directly to rate limiting and IP-based allow or deny rules at the origin. XFF is how most CDN deployments hand that address back. It is only usable when read against the list of proxies you actually trust. Many CDNs also compute attributes at the edge and pass them in dedicated headers instead. For example, Cloudflare sends the visitor's country in CF-IPCountry. That is more dependable than having the origin geolocate a value it has parsed out of XFF.

What CDNs do

  • Cloudflare sends X-Forwarded-For to the origin. If no X-Forwarded-For header was present in the request to Cloudflare, the header sent onward has the same value as CF-Connecting-IP. That is the visitor's address. If one was already present, Cloudflare appends the IP address of the HTTP proxy connecting to Cloudflare, not its own edge address. To restore the original visitor IP, Cloudflare recommends that logs and applications look at CF-Connecting-IP or True-Client-IP instead, because both have a consistent format containing only one IP address. True-Client-IP is available on the Enterprise plan only, and it is added by a Managed Transform. The Remove visitor IP headers Managed Transform suppresses these headers. For XFF, it removes the visitor IP only when the request reached Cloudflare proxied by at least one other CDN. It keeps the last proxy's address.
  • On VCL services, Fastly adds the client IP when the request is first seen by Fastly. It adds the Fastly edge POP IP when the request is received by the shield POP, if the service has shielding enabled. Fastly modifies the XFF header only on secure connections. No client IP is added if the connection from the client is not TLS. No edge POP IP is added if the backend designated for the request is not configured to use TLS. Any existing value present when the request arrives is preserved unless you set the header yourself in vcl_recv. Fastly does not modify this header in Compute services. There, you look the client IP up through the SDK instead.

Watch out for

  • Spoofing, and no authentication: anything can be written into the header before the request reaches your network. So an address in XFF is never proof of the peer that connected. If any proxy in the chain is malicious or misconfigured, any part of the header not added by a trusted proxy may be spoofed. It may also have an unexpected format or contents.
  • The trust boundary decides everything: suppose the server can be directly connected to from the internet. Even if it also sits behind a trusted reverse proxy, no part of the X-Forwarded-For list can be considered trustworthy or safe for security-related uses.
  • Security-related use: rate limits, WAF rules and IP-based access control must only use IP addresses added by a trusted proxy. Using untrustworthy values can result in rate-limiter avoidance, access-control bypass, memory exhaustion, or other negative security or availability consequences.
  • More than one header: a request may carry several X-Forwarded-For headers. They must be treated as a single list, from the first address of the first header to the last address of the last header. Using only one of them is insufficient. Some reverse proxies join them automatically, but it is safer not to assume so.
  • IPv6 and ports: IPv6 addresses may appear in XFF unquoted and without square brackets. So a parser that strips a trailing colon-port will mangle them. In Forwarded they are quoted and enclosed in square brackets. That removes the ambiguity.
  • Origin-side defaults: with nginx's real-IP module, the default real_ip_recursive off replaces the client address with the last address in the header. Behind two trusted hops, that is one of your own proxies rather than the visitor. real_ip_recursive on uses the last non-trusted address instead. That is what a CDN plus load balancer chain needs.
  • HTTP only: XFF is an HTTP request header, so it does not exist for plain TCP proxying. Cloudflare Spectrum TCP applications cannot surface these headers at the origin. The PROXY protocol is the substitute. Nginx can consume it with real_ip_header proxy_protocol.
  • Privacy: the header exposes privacy-sensitive information by design. The chain it discloses can reveal internal network structure. Keep client-IP retention deliberate. Have an egress proxy remove entries that would leak internal nodes.

Best practice

  • If you make security decisions on the value, set X-Forwarded-For yourself at your first trusted hop. Do not just extend what arrived. This way, nothing written outside your network survives into it.
  • Read right to left, past only the hops you control. Use a trusted proxy list or a trusted proxy count minus one. Leftmost, untrusted values must only be used where there is no negative impact from a spoofed value.
  • Where the CDN offers a single-IP header, use that for code that makes a decision. CF-Connecting-IP carries one address and has no list to mis-parse.
  • On the origin, configure the real-IP module with your CDN's published ranges. Enable recursive search too. Then the client address is resolved past every trusted hop, instead of from the last entry.
  • Before relying on an address, check that it is a valid address and not private or internal: spoofed values are often neither.
  • For non-HTTP traffic, carry the client address in the PROXY protocol. XFF has no equivalent below HTTP.

Examples

This Nginx setup finds the real client IP behind a trusted CDN:

# Nginx: trust your CDN and load balancer IPs
set_real_ip_from 198.51.100.0/24;  # CDN IP range
set_real_ip_from 10.0.0.0/8;       # Internal LB range
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# Now $remote_addr contains the actual client IP
log_format main '$remote_addr - $request';

Frequently Asked Questions

A de-facto HTTP request header that carries the originating client IP plus the IPs of earlier proxies as a comma-separated list, so a server behind proxies can still identify the visitor. It is not a standard and anyone can spoof it: only entries a trusted proxy added are reliable.

This Nginx setup finds the real client IP behind a trusted CDN:

# Nginx: trust your CDN and load balancer IPs
set_real_ip_from 198.51.100.0/24;  # CDN IP range
set_real_ip_from 10.0.0.0/8;       # Internal LB range
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# Now $remote_addr contains the actual client IP
log_format main '$remote_addr - $request';

Yes. X-Forwarded-For (XFF) is also known as XFF. A de-facto HTTP request header that carries the originating client IP plus the IPs of earlier proxies as a comma-separated list, so a server behind proxies can still identify the visitor. It is not a standard and anyone can spoof it: only entries a trusted proxy added are reliable.