skip to content

Compare the HTTP Forwarded header defined by RFC 7239 with X-Forwarded-For and X-Forwarded-Proto, and explain how a service behind several proxies should determine the real client IP.

level: seniorimportance: must knowfreq 55%

answer

  1. Forwarded: for / by / proto / host in one field
  2. X-Forwarded-* = de-facto split equivalent
  3. proxies append, never sanitise
  4. count trusted hops from the RIGHT
  5. X-Forwarded-Proto drives redirects and Secure cookies

basics

~20 s

Forwarded is the standard single header carrying for, proto, host and by parameters; X-Forwarded-For and X-Forwarded-Proto are the de-facto split equivalents. All are client-spoofable, so trust only entries appended by proxies you control: count hops from the rightmost end, never take the leftmost value blindly.

solid answer

~50 s

**`X-Forwarded-For`** is a comma-separated list of IPs appended to by each proxy, plus siblings `X-Forwarded-Proto` and `X-Forwarded-Host`. **`Forwarded`** (RFC 7239) standardises all of that in one field: `Forwarded: for=192.0.2.43;proto=https;host=api.example.com;by=203.0.113.7`, with support for quoted values, ports, IPv6 and obfuscated identifiers. Semantically equivalent, but `Forwarded` is properly specified and extensible; `X-Forwarded-*` is what most infrastructure actually emits. The real interview content is **trust**. Any client can send `X-Forwarded-For: 1.2.3.4`, and your edge proxy will *append* to it, not replace it. So the list is: `[attacker-supplied values..., real client IP as seen by proxy 1, ...]`. Correct extraction: know how many trusted proxies sit in front (N), then take the **(N+1)th value from the right**, or walk right-to-left skipping addresses in your trusted CIDR set. Never take the leftmost element on a public-facing service. Get this wrong and rate limiting, geo-blocking, IP allowlists and audit logs are all trivially spoofable. Frameworks expose this as a trusted-proxy or trusted-hop-count setting — configure it explicitly.

code

http · 6 lines
http
GET /admin HTTP/1.1
Host: api.example.com
X-Forwarded-For: 10.0.0.5, 203.0.113.9, 198.51.100.4
X-Forwarded-Proto: https

Forwarded: for=10.0.0.5, for=203.0.113.9;proto=https, for=198.51.100.4;by=203.0.113.7

go deeper

for a junior

Know what the headers are for — original client IP and scheme when a proxy sits in front — and that the app's socket peer is only the proxy.

for a middle

Explain the Forwarded parameters versus the X-Forwarded-* split, that proxies append to a list, and that the values come from the network and are not trustworthy by default.

for a senior

Give the extraction algorithm with trusted hop counts or CIDRs from the right, name the controls that break when it is wrong, and cover X-Forwarded-Proto driving redirects and Secure cookies.

for a principal

Decide the platform trust boundary: which edge authoritatively sets client identity, whether to normalise to one header, privacy of logged addresses, and how abuse controls degrade when the edge topology changes.

## What the fields carry When a request passes through reverse proxies, the origin's socket only sees the last proxy. The lost facts — original client IP, original scheme, original Host — are re-added as headers. **De-facto set:** - `X-Forwarded-For: 203.0.113.9, 198.51.100.4` — client first, then each proxy appends the peer address it saw. - `X-Forwarded-Proto: https` — the scheme the *client* used, which matters because TLS usually terminates at the edge and the app sees plain HTTP. - `X-Forwarded-Host`, `X-Forwarded-Port` — the original authority. **Standard (RFC 7239):** one `Forwarded` field, a comma-separated list of hop elements, each a semicolon-separated set of parameters: `Forwarded: for=192.0.2.43;proto=https;host=api.example.com, for="[2001:db8::1]:4711";by=203.0.113.7` Parameters: `for` (client of that hop), `by` (the interface that received it), `proto`, `host`. Values may be quoted, may include ports, IPv6 must be bracketed and quoted, and **obfuscated identifiers** like `for=_hidden` are allowed for privacy — a real advantage over the X- form, which can only carry raw addresses. ## Why both exist `X-Forwarded-For` predates any standard and became universal, so RFC 7239 could not simply rename it — the whole cautionary tale RFC 6648 warns about. Practically: emit and accept `X-Forwarded-*` because that is what CDNs, ALBs and nginx produce, and accept `Forwarded` too if anything upstream emits it. Do not merge blindly; if both are present, decide by policy which one your trusted edge sets, and ignore the other. ## Determining the client IP safely The critical property: **proxies append, they do not sanitise**. If a client sends `X-Forwarded-For: 1.2.3.4` and your CDN adds the true peer, the origin receives `1.2.3.4, <real client>`. Taking the first element hands the attacker complete control of "the client IP". The correct algorithm: 1. Know your topology: exactly how many proxies you control sit in front of the app (call it N), or the CIDR set of their addresses. 2. Parse the list left to right in append order; the rightmost entry was added by the closest proxy. 3. Walk from the **right**, discarding entries that came from trusted proxies. The first untrusted address you reach is the client. 4. Equivalently, with a fixed chain of N trusted hops, take the (N+1)th element from the right. 5. If the list is shorter than expected, do not fall back to the leftmost value — treat it as untrusted and fall back to the socket peer address. Extra care: strip whitespace, handle IPv6 with brackets and ports, cap the list length (a client can send hundreds of entries and blow up your parser or your header budget), and reject malformed entries rather than coercing them. ## What depends on getting it right - **Rate limiting and abuse controls** keyed on a spoofable IP are no controls at all; an attacker rotates the header and each request looks like a new client. - **IP allowlists** protecting admin endpoints can be bypassed outright. - **Geo-blocking and fraud signals** become attacker-chosen. - **Audit logs** record whatever the attacker wanted them to record. - **`X-Forwarded-Proto`** drives redirect and cookie decisions: an app that sees plain HTTP behind a TLS-terminating edge will build `http://` absolute URLs and may refuse to set `Secure` cookies unless it honours this header. Trusting it from an untrusted source, conversely, lets a client claim `https` and defeat force-HTTPS logic. ## Configuration in practice - Frameworks expose it: a trusted-proxy list or hop count. Set it explicitly; defaults are usually "trust nothing" (safe but wrong behind a load balancer) or "trust everything" (convenient and exploitable). - The **edge you control should overwrite, not append**, the client-supplied value for its own hop where possible — many CDNs offer a "true client IP" header they set authoritatively for exactly this reason. - Anything not from a trusted hop should be treated as user input: length-capped, format-validated, and never interpolated into logs or SQL unchecked. - Same discipline for `X-Forwarded-Host`: honouring an attacker-supplied host is how password-reset links get poisoned. ## Summary comparison `Forwarded` is the standard, single, extensible, privacy-aware field; `X-Forwarded-*` is the split, ubiquitous, less rigorous one. Neither is authenticated. The security property does not come from the header choice — it comes from knowing which hops you trust and counting from the right end.

  • Why is taking the leftmost X-Forwarded-For entry dangerous?
    Proxies append rather than replace, so any value the client itself sent survives at the left of the list. The leftmost element is therefore attacker-controlled on any public endpoint, and using it for rate limiting, allowlists or audit logs lets a caller impersonate any address. The trustworthy portion is the tail written by proxies you operate, so you count in from the right.
  • What can go wrong if the application ignores X-Forwarded-Proto behind a TLS-terminating load balancer?
    The app sees plain HTTP, so it generates http:// absolute URLs in redirects and links, may refuse to mark cookies Secure, and can create redirect loops with force-HTTPS rules. Honouring the header from a trusted edge fixes it, but the same header from an untrusted source lets a client falsely claim https and bypass HTTPS enforcement, so it must only be trusted from known proxies.
  • How does Forwarded improve on X-Forwarded-For beyond being standardised?
    It carries all the hop facts in one field with a defined grammar, supports quoted values, ports and bracketed IPv6 cleanly, records the receiving interface with the by parameter, and permits obfuscated identifiers so a proxy can indicate a hop without disclosing an address. It is also extensible with new parameters rather than requiring a new X- field per fact.

saying these in an interview costs you the question

  • Using the first entry of X-Forwarded-For as the client IP on a public service
  • Believing proxies overwrite the header, so a client-supplied value cannot survive
  • Trusting X-Forwarded-Proto or X-Forwarded-Host from any source, including direct clients
  • Assuming Forwarded is authenticated or harder to spoof than X-Forwarded-For
  • Rate limiting by an unvalidated forwarded IP and calling it an abuse control

context