-
Notifications
You must be signed in to change notification settings - Fork 18k
net/http: Header.Get should return the last header rather than the first #51493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Note
Changing it to get the last value is also considered a breaking change. On cc @neild |
As @seankhliao says, I'm dubious that any API change or vet check can compensate for the need to exercise extreme care around using client-provided headers for security purposes. |
I think you're conflating the "last header value" with the "last value in the comma-separated list within that header". Using the last value would be wrong, but you'd still be counting from the right so you'd still want the last header. ...Except when your trusted proxy IPs cross header boundaries. Which makes me think of another, probably better, API addition suggestion: Add a method that returns all of the headers with the same name combined into a comma-separated list. This is per RFC. It's also what some reverse proxies already do (like AWS ALB). And it makes it easy to correctly get the N-from-rightmost XFF IP. |
It doesn't really matter which one you get, the point was you cannot trust/use the information in there unless your proxies sanitize / inject extra information. Structured headers are #41046 |
Yeah, but that's part of my point. If you use the last header you're much more likely to be using the header with the values added by your own proxies than you will be if you use the first. So the current Header.Get design is less-secure-by-default.
I know. Part of my goal is to surface the problem to the general consciousness. There are a lot of projects doing this wrong -- even big ones -- and this bit of the stdlib design is part of the problem. |
The method Get is working is documented. I think the only thing we could do is add a warning to the documentation of .Get and/or of Headers about the security risks involved. |
That is probably the optimal realistic outcome. |
Related: #50465 |
Also, I'd like to suggest that an alternative here would be to add a new
|
Is the last header really the best one, though? In this case all headers should probably be considered. |
The point is, it depends on the usecase whether the first or last or all values should be considered. But right now, Go's API only makes it easy to look at the first or get a slice of all the values, and getting the last requires doing a bit more work: func lastHeaderValue(h http.Header, field string) (value string, ok bool) {
values := h.Values(field)
if len(values) == 0 {
return "", false
}
return values[len(values)-1], true
} |
While I think that "get last" is better than "get first", it's still not ideal. For example, getting the "rightmost" XFF IP is actually "count from the right depending on number of trusted proxies; or search from the right for the first IP that is not on the 'trusted' list; or search from the right for the first non-private/internal IP". That means that the desired IP might not actually be in the last header, since the search might need to go back further. If we're considering a new method, then I think that the best "get single header value" would be either:
This is partly inspired by looking at NodeJS's
So the default is comma (good for XFF; adheres best to RFC 2616), but |
Since we're not going to change Get to turn return the last header, that leaves us with returning some merged list of structured header values. I think this can be rolled up into #41046 |
@seankhliao my suggestion is rather to add |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes.
Description
I recently did research into the misuse of the
X-Forwarded-For
header to get the "real" client IP, especially by rate limiters. Of the six Go-based rate limiter implementations I looked at, every single one was usinghttp.Header.Get
and running the risk of falling prey to spoofing, resulting in rate limiter escape and memory exhaustion. (Concrete non-redacted example here.)According to the HTTP/1.1 RFC (2616)1:
I think that's saying that the order of same-name headers is important for "comma-separated list" headers and no others (since there shouldn't be multiple headers for non-list headers). So whether Header.Get returns the first or last header doesn't matter for non-list headers.
Two such "comma-separated list" headers are
X-Forwarded-For
andForwarded
. With those headers, the leftmost part of the list is completely untrustworthy, while the rightmost is added by trusted reverse proxies (if they exist). So it matters very much whether Header.Get returns the first or the last.In general, if the "real" client IP is to be used for anything remote security-related, it must be the rightmost value that's used (or rightmost-ish, depending on number of reverse proxies). The leftmost value can be used, but since it's trivial to spoof (just add the XFF header to the request), it must be done with great care.
So, I argue that:
X-Forwarded-For
(weasel words: "one of")X-Forwarded-For
is (or should be) to take the rightmost value of all present XFF headersI haven't done a comprehensive survey of different multi-headers, so I can't make those weasel-word statements more concrete.
As an example from another language, Twisted uses the last header value when calling the equivalent of Header.Get.
(For even more thoughts about multiple XFF header problems, see my blog post.)
The simplest fix for this would be change the behaviour of Header.Get. This would cause problems with code that expects to get the first header -- the obvious example being code that actually wants the leftmost XFF IP value.
Another approach would be to add a Header.GetLast function. Unfortunately, a lot of people would still naively pick Header.Get, and it obviously wouldn't fix any existing incorrect uses of Header.Get. It would still be an improvement, however -- right now people look at the API and thing "I don't want a slice, just a value" and they have only one choice.
Another possibility might be to add a go vet check for Header.Get combined with "split by comma" combined with "take last". (Except sometimes it's "reverse-find command and subslice from there".) (I don't know anything about how go vet works, so no idea if that's feasible.)
Another approach would be to break the API and add an argument to Header.Get. But that's obviously not going to happen.
ETA another suggestion I thought of below: Add a method that returns all of the headers with the same name combined into a comma-separated list. This is per RFC. It's also what some reverse proxies already do (like AWS ALB). And it makes it easy to correctly get the N-from-rightmost XFF IP.
Footnotes
I believe this is inherited unchanged into HTTP/2. ↩
The text was updated successfully, but these errors were encountered: