Skip to content

maxuru ~ % cat haproxy/trust-x-forwarded-for

Handle X-Forwarded-For so it can't be spoofed

Severity: mediumApplies to: HAProxy 2.xApplies to: HAProxy 3.x
The fix/etc/haproxy/haproxy.cfg
frontend web
# Overwrite anything the client sent, then append the real source
http-request del-header X-Forwarded-For
option forwardfor

Delete first, then let forwardfor add the value HAProxy actually observed.

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.1
X-Forwarded-For: 10.0.0.1

the backend receives:

X-Forwarded-For: 10.0.0.1
X-Forwarded-For: 203.0.113.45

The 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.1 because 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.

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.

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/8