Summary

  • Forwarded and X-Forwarded-For carry path claims, not authenticated client identity. The immutable starting observation is the immediate transport peer; earlier addresses become usable only through an explicit proxy-trust rule.
  • Safe systems preserve raw fields, parser and trust-policy versions, the first untrusted boundary and the downstream decision. A syntactically valid address is not authority for an allowlist, rate limit or irreversible action.

The parser was right

The incident report initially blamed “spoofed headers.” That description was too shallow. Headers are inputs; clients are allowed to send inputs. The defect was that an origin exposed outside its intended edge path treated the first item in a comma-separated list as a fact about the connection.

The useful local fact was simpler: the TCP peer was an address that was not one of the authorised reverse proxies. At that point the chain should have ended. No amount of syntactic validity to the left could make that peer a witness.

This distinction matters because an implementation can parse every delimiter correctly and still make a false decision. The failure does not live in the grammar alone. It lives in the promotion from a claim to authority.

What the standardized field says

RFC 7239 defines Forwarded as an optional HTTP field for disclosing information that a proxy changes or hides. Each element can include for, identifying the node that made the request to a proxy; by, identifying the proxy interface; host, carrying the received host information; and proto, carrying the protocol used on an earlier leg.

Those parameters are correlated inside an element, but they are not one identity. for=192.0.2.4;proto=https does not prove that a person at that address used TLS, much less that the person is entitled to an account. It says what a forwarding node asserts about a particular hop.

RFC 7239 is unusually direct about integrity: the field cannot be relied upon to be correct because every node on the path, including the client, can change it. Whitelisting verified proxies can protect the information they add, but it does not validate an arbitrary prefix that arrived before them. If the link from a trusted proxy to the endpoint is not secured, even the protected suffix may be altered in transit.

The standard also allows unknown and obfuscated node identifiers. These are not malformed IP addresses waiting to be coerced. They are explicit limits on disclosure. A system that needs a numerical address must preserve the difference between “not disclosed,” “obfuscated,” “invalid” and “absent.”

Forwarded is not XFF

X-Forwarded-For is older, widely deployed and not defined by RFC 7239. Both often form comma-separated chains, but their grammars and product behavior are not interchangeable. An estate that says “we support forwarded headers” without naming the exact field, generating proxy, append rule and consuming parser has not described a control.

AWS Application Load Balancer illustrates the choice. Its documented XFF modes append, preserve or remove. In append mode, an incoming header is retained and the address observed by the load balancer is added at the right. The load balancer's participation therefore does not bless the values already present to its left.

HAProxy likewise documents an insertion mode and tells downstream servers which occurrence its configuration expects them to use. It separately supports the RFC 7239 field. A downstream application cannot safely import an algorithm from another product or assume that “first” and “last” have universal meanings.

Begin at the connection

The immediate socket peer is not necessarily the original user. In a healthy reverse-proxy architecture it is often a load balancer. But it is the observation the receiver obtained from its own transport stack, rather than from HTTP text. That makes it the anchor for delegated claims.

The receiver first asks whether this peer is permitted to assert forwarding metadata. If not, it uses the peer address or rejects the route. If yes, it applies the configured algorithm to the chain the peer supplied. It then walks only as far as authorised topology permits, stopping at the first untrusted node.

NGINX makes this relationship explicit. set_real_ip_from defines addresses known to send correct replacement values. With real_ip_recursive on, the chosen client address is the last non-trusted address in the configured header; $realip_remote_addr retains the original peer. The two variables are evidence of different things.

Apache mod_remoteip gives the same warning in sharper operational language: it is trivial for an untrusted user agent to impersonate another address if the behavior is enabled without a trusted intermediary. Apache processes the presented list from right to left and keeps the connection address separately.

Envoy exposes another version of the same boundary. Its trusted-hop setting counts from the right side of XFF, with details depending on use_remote_address; a CIDR-based extension is available when topology is better expressed as known networks. These are not decorative knobs. An incorrect hop count changes which speaker is treated as a witness.

There is no universal command called “get the real IP.” There is only a locally verified peer, a stated topology, a field contract and an algorithm whose failure modes must be tested.

Lists are evidence structures

RFC 9110 says repeated field lines that support list syntax are combined in received order, separated by commas. That means order is semantically significant. It also means a proxy, HTTP library and application framework may expose different representations of the same request: raw field lines, one combined string, or a parsed array.

A security review needs to send duplicates deliberately. If an edge validates the last line while an application consumes the first, the deployment has two authority models. If a log collector reorders elements, the record cannot explain the decision it claims to audit.

Address syntax creates another fault line. IPv6 contains colons; a port adds another delimiter; RFC 7239 can require quotes and brackets. A naive split(':') parser can turn a valid address into fragments or make a log disagree with the access-control library. Tests should include quoted IPv6, IPv6 with port, IPv4 with port, obfuscated nodes, unknown, whitespace variants and malformed delimiters.

Canonical parsing is necessary. It still does not establish trust. The system must record both results: “this is the normalized value” and “this is why this sender was authorised to supply it.”

host and proto do not inherit authority

Forwarding fields can also restore the host and scheme seen before TLS termination. Applications use these values for redirects, absolute URLs, secure-cookie decisions and origin policy. The same rule applies: a client-supplied proto=https does not prove that a trusted edge received TLS, and a forwarded host does not authenticate the requested origin.

Each parameter needs its own consumer and trust decision. A deployment may trust an edge to assert the observed client address but derive the public host from a fixed route map. Another may accept proto only from a mutually authenticated internal proxy. Treating the whole field as trusted because one parameter is useful broadens authority invisibly.

PROXY protocol is another ledger

The PROXY protocol carries source and destination information before the application protocol. It can preserve an address across a TCP or TLS load balancer without using an HTTP header. It is not a stronger claim merely because it appears earlier on the wire.

The receiver must expect the preface on a dedicated or strictly controlled listener and accept it only from authorised senders. Otherwise an arbitrary network client can write a preface that names any source address. The transport ledger should record the actual sender peer, whether the preface was expected and accepted, and the address it supplied. Only then may HTTP processing begin.

Collapsing PROXY data, socket peer, XFF and RFC 7239 into one variable called client_ip destroys the chain of custody. Convenience variables can exist, but the original observations and derivation reason must remain available.

The negative test is the specification

Start outside the edge and attempt to reach the origin directly. If the architecture says that path cannot exist, the packet should fail before HTTP. Then send an allowlisted address in the first XFF position. The application must not inherit it from an unauthorised peer.

Through the legitimate edge, prepend a forged address. Confirm the edge's sanitise or append behavior and prove the resolver stops at the intended boundary. Add an unexpected proxy, remove an expected proxy and rotate a proxy address outside the trusted range. Fixed-hop systems must fail visibly when path length changes.

Send duplicate lines and pathological but valid address forms. Compare what the edge, origin, framework, rate limiter and logger believe. Then send a PROXY preface from an unauthorised source. A configuration file that appears correct is not evidence that all those runtime consumers agree.

Finally, test the identity limit. Two authenticated accounts can share one NAT address; one account can move across several networks. Even a correctly resolved source address is contextual evidence, not a durable person or account identifier.