skip to content

When a web framework is told to trust forwarded headers, what changes about the client address, scheme and host it reports?

level: seniorimportance: should knowfreq 58%

answer

  1. connection facts versus claimed facts
  2. same accessors, different source
  3. headers are caller-written text
  4. trust list names the proxy
  5. configuration, not compiled-in

basics

~20 s

The same accessors stop describing the immediate connection and start reporting what forwarded header fields claim about the original client. Those fields are ordinary request text, so the framework believes them only where it is configured to trust the peer.

solid answer

~40 s

By default the request object describes the **hop it is actually on**: the peer address is whoever opened the connection — behind a proxy, that is the proxy — and the scheme and host reflect how that hop received the request. Turning on forwarded-header support adds no new accessors; it changes what the existing ones return, reconstructing address, scheme and host from fields such as `Forwarded`, `X-Forwarded-For` and `X-Forwarded-Proto`. Those fields are just text in the request and any caller can write them, so the feature is gated on a **trusted-proxy setting**: a list of peer addresses, or a hop count, naming which connections may speak for someone else. Enabled on a directly reachable service, it lets the caller choose the address, scheme and host that your rate limits, allow-lists and audit logs see.

go deeper

for a junior

Remember that the address and scheme the framework reports describe the connection it received, which behind a proxy is the proxy's connection, not the client's.

for a middle

Explain that enabling forwarded-header support does not add accessors but changes what the existing ones return, and that the source becomes text the caller could have written.

for a senior

Tie the setting to the deployment: trust only named peers, keep the direct path unreachable, and test that a forged forwarded field on a direct call is ignored. Know which failure is safe and which is not.

for a principal

Treat it as a trust boundary owned by the platform, not by each service: one configured answer per environment, revisited whenever a hop is added, with everything derived from it inheriting that decision.

## What the request reports by default Three of the things a request object exposes are not read from the message body of the request at all — they describe the **hop the framework is actually on**: - the **peer address**, which is whoever opened the connection; - the **scheme**, which is how that connection was received (encrypted or not); - the **host and port** the request was addressed to. Deployed behind a proxy, load balancer or gateway, that hop is the proxy. The peer address is the proxy's address, and the scheme is the scheme of the internal connection, which is frequently plain even when the client used an encrypted one. The framework is not lying: it is reporting the connection it has. It simply does not have the client's. ## What trusting forwarded headers changes Enabling forwarded-header support does not add a second set of accessors. It **changes what the existing ones return**: address, scheme and host are reconstructed from header fields the upstream hop wrote — the standard `Forwarded` field, or the widely deployed `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host`. Code that reads the client address keeps compiling and starts answering a different question. That is the entire feature, and it is why it is dangerous: a value that used to be a fact about the transport becomes a claim in text. ## Why it must be gated Header fields are just request text. Any caller can write any of them. Nothing in the protocol stops a client from asserting an address it does not have, a scheme it did not use, or a host it was not sent to. So the framework must be told **whose claims to believe**, and that is what a trusted-proxy setting is: a list of peer addresses (or a count of hops) that says which connections are allowed to speak on behalf of someone else. A claim that arrives on a connection from outside that set is ignored and the connection facts stand. The gate is the whole of this leaf's concern. *How* a multi-hop chain is written and which entry is the real client are questions about the header fields themselves; the framework question is simply **do we believe them at all, and on whose word**. ## Deployment-shaped, not code-shaped Whether a forwarded claim is credible depends on what sits in front of the process, and that differs between a local run, one environment with a single proxy and another with an edge plus an internal balancer. The same build must believe headers in one place and ignore them in another, so the trust decision belongs in **configuration**, not in a compiled-in flag. Two practical consequences: 1. A service that is reachable both through the proxy and directly — from inside the network, by a health checker, or because a security group is wider than intended — receives forwarded headers on the direct path too, written by whoever called it. 2. Adding or removing a hop changes the answer. A trust setting expressed as "skip one hop" silently becomes wrong the day a second proxy is introduced. ## The four cells | | Trust disabled | Trust enabled | |---|---|---| | **Reachable directly** | correct: accessors describe the real peer | spoofable: the caller dictates address, scheme and host | | **Behind a trusted proxy** | safe but useless: every client looks like the proxy | correct, provided the trust list names that proxy | The diagonal is what matters. The bottom-left cell is a *safe* failure — per-client logic degrades to nonsense, but nothing can be forged. The top-right cell is an *unsafe* failure: allow-lists, per-client rate limits, geo decisions, audit trails and any check of "was this request encrypted" become attacker-controlled. ## Getting it right operationally - Keep the setting off by default and turn it on only where a proxy is genuinely in front. - Name the proxy, do not trust everything. "Trust all sources" restores exactly the vulnerability the setting exists to prevent. - Make the direct path unreachable, so the trusted deployment is the only deployment. - Assert the deployment: a smoke test that calls the service directly with a forged forwarded header and checks that the value is ignored catches the misconfiguration before an audit does. - Remember that every value derived from the setting — the address in your logs, the scheme used to build absolute links, the host in a canonical URL — inherits the same trust decision.

  • Why is the trusted-proxy list a deployment setting rather than something the code decides?
    Because whether a forwarded claim is credible depends on what sits in front of the process, and that differs per environment: nothing locally, one proxy here, an edge plus an internal balancer there. The same build has to believe the fields in one place and ignore them in another, so the trust boundary lives in configuration.
  • Every client appears with the same address in your logs. What is the likely cause, and how bad is it?
    Either forwarded-header support is off or the trust list does not include the proxy, so the accessor is honestly reporting the peer that opened the connection — the proxy itself. It is the safe failure: per-client logic degrades to noise, but nothing can be forged through values nobody believes.

saying these in an interview costs you the question

  • Trusts forwarded fields everywhere because a proxy is usually in front
  • Believes those values are added by the network and cannot be forged
  • Enables the setting in code so every environment behaves identically
  • Rate-limits or allow-lists on an address the caller can choose
  • Assumes a request that bypassed the proxy cannot carry forwarded fields