maxuru ~ % cat haproxy/trust-x-forwarded-for
Handle X-Forwarded-For so it can't be spoofed
/etc/haproxy/haproxy.cfgfrontend web # Overwrite anything the client sent, then append the real source http-request del-header X-Forwarded-For option forwardforDelete first, then let forwardfor add the value HAProxy actually observed.
Why appending is the problem
Section titled “Why appending is the problem”option forwardfor inserts the client’s address into X-Forwarded-For.
HAProxy’s documentation is precise about how: the header “is always appended at
the end of the existing header list”, and it notes that “it is really possible
that the client has already brought one.”
So if a client sends this:
GET / HTTP/1.1X-Forwarded-For: 10.0.0.1the backend receives:
X-Forwarded-For: 10.0.0.1X-Forwarded-For: 203.0.113.45The first value is attacker-controlled. The second is real. Nothing in the request marks which is which — the backend has to know the convention.
This turns into a vulnerability the moment something acts on that address:
- Allow-lists. A backend that admits requests from
10.0.0.1because it is “internal” can be reached by anyone willing to type that header. - Rate limiting. Per-IP limits keyed on a spoofable value are trivially evaded by varying it.
- Audit logs. The address in your incident timeline is whichever one the application happened to pick up.
Two fixes
Section titled “Two fixes”Delete then append, as in the fix block above, is the one to prefer at an
internet-facing edge. After del-header, exactly one X-Forwarded-For reaches
the backend and it is the one HAProxy observed. Nothing downstream has to know
the convention.
Read the last occurrence is the alternative when you cannot change the proxy — HAProxy’s docs state that “the server must be configured to always use the last occurrence of this header only.” This is correct but fragile: it relies on every consumer, including frameworks and middleware you did not write, agreeing to count from the right. Many default to the leftmost value precisely because that is the original client in a well-behaved chain.
If HAProxy is behind another proxy or a CDN, deleting the header discards a
real address you need. There, keep the chain and configure the application with
the number of trusted hops, or use the standardised Forwarded header via
option forwarded.
Do not reach for if-none
Section titled “Do not reach for if-none”option forwardfor if-none only adds the header when one is not already
present — which means a client that supplies its own keeps it, unmodified.
HAProxy’s documentation attaches an explicit warning: it “should only be used
in perfectly trusted environment, as this might cause a security issue if
headers reaching HAProxy are under the control of the end-user.”
On an internet-facing frontend, headers reaching HAProxy are by definition under
the control of the end user. if-none is for chained proxies on a private
network, and even then except is usually the better tool:
option forwardfor except 127.0.0.0/8Related
Section titled “Related”- Request smuggling — the other way a backend can be told something untrue about a request.
- nginx rate limiting — the same trust question, from the limiter’s side.