[HN Gopher] What is X-Forwarded-For and when can you trust it? (...
___________________________________________________________________
What is X-Forwarded-For and when can you trust it? (2024)
Author : ayoisaiah
Score : 62 points
Date : 2025-07-23 10:18 UTC (3 days ago)
(HTM) web link (httptoolkit.com)
(TXT) w3m dump (httptoolkit.com)
| westurner wrote:
| From the article: https://httptoolkit.com/blog/what-is-x-
| forwarded-for/ :
|
| > _Dropping all external values like this is the safest approach
| when you 're not sure how secure and reliable the rest of your
| call chain is going to be. If other proxies and backend apps are
| likely to blindly trust the incoming information, or generally
| make insecure choices (which we'll get into more later) then it's
| probably safest to completely replace the X-Forwarded-For header
| at that outside-world facing reverse proxy, and ditch any
| untrustworthy data in the process._
|
| X-Forwarded-For: https://en.wikipedia.org/wiki/X-Forwarded-For :
|
| > _Just logging the X-Forwarded-For field is not always enough as
| the last proxy IP address in a chain is not contained within the
| X-Forwarded-For field, it is in the actual IP header. A web
| server should log both the request 's source IP address and the
| X-Forwarded-For field information for completeness_
|
| HTTP header injection:
| https://en.wikipedia.org/wiki/HTTP_header_injection
|
| This OWASP page has a list of X-Forwarded-For and X-FORWARDED-foR
| and similar headers; "Headers for IP Spoofing"
| https://owasp.org/www-community/pages/attacks/ip_spoofing_vi...
|
| A sufficient WAF should detect all such attempts.
|
| The X-Forwarded-For Wikipedia article mentions that RFC 7239
| actually standardizes the header and parsing:
| Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
| Forwarded: for="[2001:db8::1234]"
|
| RFC 7239: "Forwarded HTTP Extension" (2014): https://www.rfc-
| editor.org/rfc/rfc7239
| vlod wrote:
| If I remember correctly, it's sometimes used by nginx (used as a
| reverse proxy) to inject values into the request of the real ip
| address. e.g. nginx in front of several node processes.
|
| I've had problems with this though. I want to get the ip4 address
| from cloudflare, instead of ip6 and it's next to impossible
| AFAICT. (for the free plan anyway)
| remram wrote:
| A request doesn't come in with both an IPv4 and an IPv6. If the
| user connected over IPv6, the connection only has an IPv6
| address. You can't get the IPv4 address, there is none.
| miyuru wrote:
| what the usecase and what is the problem using the IPv6
| address?
|
| In fact if the user has IPv6, IP blocks/Rate limits wont affect
| the other users on the CGNAT legacy address.
| supriyo-biswas wrote:
| > I've had problems with this though. I want to get the ip4
| address from cloudflare, instead of ip6 and it's next to
| impossible AFAICT. (for the free plan anyway)
|
| I have multiple websites on Cloudflare and can receive IPv4
| addresses just fine, though for Cloudflare fronted websites
| it's usually better to use the CF-Connecting-IP as people can
| send any value in the X-Forwarded-For header.
|
| Maybe some intermediate layer turns the IPv4 into a IPv6-mapped
| IPv4 address?
| nodesocket wrote:
| I know that the Python module proxy_fix[1] requires you to
| configure how many X-Forwarded-For ip entries it should trust
| with the default being 1.
|
| [1]
| https://werkzeug.palletsprojects.com/en/stable/middleware/pr...
| SSchick wrote:
| See https://expressjs.com/en/guide/behind-proxies.html for a fun
| read.
| francislavoie wrote:
| A very in-depth article on the topic:
| https://adam-p.ca/blog/2022/03/x-forwarded-for/
|
| I implemented those recommendations in Caddy to enable a "trusted
| proxies" system which informs the proxy, logging, request
| matching etc to safely use the client IP taken from proxy
| headers.
| danhite wrote:
| The article you cited was very informative.
|
| For example it reminds that standards & intermediaries you
| trust might still allow multiple unconcatenated XFF headers
| though and the XFF unpeel by right to left heuristic fails if
| you only look at the e.g. the first such header.
|
| This reminded me that there always seems to be a past or future
| pitfall case lurking, such as using old code/library honoring a
| X-HTTP-Method-Override header (e.g. historically used to
| ~convert POST to PUT bypassing client CORS restrictions).
|
| Many holes I've noticed over the years seem to spring from
| legacy preservation of well intended endruns of restrictions,
| for example I can presently in Safari exploit a bug to get a
| crypto digest calculated for me within an insecure context ...
| I find this quite usefully locally (e.g. lan http: , data: ,
| ~bookmarklets that I control) but nonetheless I reported the
| bug and am trying not to rely on it.
| OutOfHere wrote:
| This was the only article that opened my eyes; it's a lot
| better than the Wikipedia article on the topic. It covers the
| XFF header, also related headers, clearly from both defense and
| offense perspectives.
|
| Two things it failed to advise for defense are:
|
| (1) I can simply just reject requests that provide multiple
| keys of this header. In Go, I will use
| `http.Header.Values(headerName)` to check the count. There is
| no good reason for having multiple keys of it. Any
| misconfiguration in setting them is the client's problem.
|
| (2) I can and I must reject large requests that have too many
| header bytes. In Go, when initializing `http.Server`, I can
| give it the `MaxHeaderBytes` argument. Sending megabytes of
| headers stops here.
|
| If I understood correctly, when wanting the rightmost-ish XFF
| value, I can use the rightmost value that is not in a list of
| trusted subnets, assuming there is at least one remaining value
| left of it.
| pityJuke wrote:
| Brings back memories of changing the header to watch South Park
| episodes for free (the official site was very vulnerable to just
| changing the header to a NA IP).
| 0points wrote:
| X-Forwarded-For lets us bypass geoblocking ;-)
| wutwutwat wrote:
| A properly configured load balancer is going to drop this
| header if the client sends it, and then set it itself, with the
| request connection's ip being first, then the proxy ip being
| second. Every proxy after that should append its own ip to that
| header, then finally when the request reaches your app server,
| you should filter out your known proxy ips to be left hopefully
| with just the ip address of the connection the request was
| forwarded for, which was not set via any client header, and not
| able to be spoofed.
|
| I'm sure plenty of lbs/reverse proxies and app servers don't
| set things, establish trust, or filter the header properly
| though, because, people, but it is easy to lock down.
| tetha wrote:
| Yeah we got dinged by our pentesters a few years ago because
| the LB didn't clear X-Forwarded-For headers. So you could
| just set some trusted IP into the X-Forwarded-For header and
| various ip whitelists went "Well, it came from there, so we
| gonna let it though".
|
| Oops :)
|
| It is one of these trust-based headers that need to be
| cleared at the edge of your network / trust zone.
| OutOfHere wrote:
| I do not agree that the XFF header must be dropped and re-
| set. Doing so can in fact be harmful. There is a reason for
| preserving the chain of IPs, which is that it allows the app
| to use the rightmost-ish IP after skipping the known proxy
| IPs.
| knorker wrote:
| 1. Have (and maintain!) a list of addresses you trust to not lie
| (e.g. your own proxy layers, cloudflare's proxy IP list, akamai,
| GCP LB, AWS LB, etc...)
|
| 2. If the connecting party (real TCP connection remote end) is in
| the trusted list, then take the rightmost address in XFF and set
| as remote end.
|
| 3. Repeat 2 until you get an address not in the trusted list.
|
| 4. That is now the real client IP. Discard anything to the left
| of it in XFF. (though maybe log it, if you want)
|
| The article seems to forget the step of checking the real TCP
| connection remote address (from my skimming), which means that if
| the web server can be accessed directly, and not just through a
| load balancer that always sets the header, then the article is a
| security hole.
| danhite wrote:
| thank you for your comment :
|
| > The article seems to forget the step of checking the real TCP
| connection remote address (from my skimming)
|
| as this alerted me when reading the article to see their very
| important, but not highlighted, caveat emptor that covers this
| dangerous case : Note that this logic assumes
| that your server is not directly accessible. If it is,
| you need to check the actual request source IP address is one
| of yours first - effectively treating that as an extra
| right-most address.
| nfriedly wrote:
| I have a rate limiting library, and for a long time, some of the
| most frequent issues related to misconfiguration around
| X-Forwarded-For headers: either ignoring them when it shouldn't
| and limiting the load balancer's IP instead of the end user, or
| blindly trusting any XFF header and allowing limits to be
| trivially bypassed.
|
| Eventually I added some runtime checks that log a warning and
| linked to documentation for both of those issues and a few other
| common ones.
|
| My support burden has decreased dramatically since then.
| MajesticHobo2 wrote:
| XFF handling is the bug that keeps on giving. I'd estimate I've
| seen incorrect parsing of it in at least half of the web
| applications I've audited professionally.
|
| The funniest is when the app renders user IP addresses somewhere
| and you can get XSS through it.
| Avamander wrote:
| Side note, don't use XFF if you have any option not to. The
| "Forwarded" header is much nicer.
| OutOfHere wrote:
| The XFF header is set a lot more commonly, and this gives the
| app the freedom to be implicitly compatible with a lot more
| reverse proxy servers than the Forwarded header without needing
| special configuration.
|
| Moreover, the Forwarded header has all the security pitfalls of
| the XFF header.
___________________________________________________________________
(page generated 2025-07-26 23:02 UTC)