Skip to content
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

net/http/httputil: NewSingleHostReverseProxy does not preserve X-Forwarded-Proto header #57132

Closed
willchan opened this issue Dec 7, 2022 · 8 comments
Assignees
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. release-blocker
Milestone

Comments

@willchan
Copy link
Contributor

willchan commented Dec 7, 2022

What version of Go are you using (go version)?

dev branch, which as of time of writing, should be 1.20

Does this issue reproduce with the latest release?

Does not repro in 1.19, which is the latest stable release as of time of writing.

What operating system and processor architecture are you using (go env)?

https://go.dev/play

What did you do?

https://go.dev/play/p/T80FZx8gCPU?v=gotip

What did you expect to see?

X-Forwarded-Proto: https should be preserved in the backend, like on 1.19: https://go.dev/play/p/L6Z97pkbuP2.

What did you see instead?

X-Forwarded-Proto got overwritten with http.

It looks like this is overwritten here. It looks like the intent of the code is, if X-Forwarded-Proto is not present, to infer from http.Request.TLS whether X-Forwarded-Proto should be set to http or https. However, it's doing this even when X-Forwarded-Proto is present.

@mengzhuo
Copy link
Contributor

mengzhuo commented Dec 7, 2022

cc @neild

@mengzhuo mengzhuo changed the title httputil: NewSingleHostReverseProxy does not preserve X-Forwarded-Proto header net/http/httputil: NewSingleHostReverseProxy does not preserve X-Forwarded-Proto header Dec 7, 2022
@prattmic prattmic added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Dec 7, 2022
@seankhliao seankhliao added this to the Go1.20 milestone Dec 7, 2022
@AlexanderYastrebov
Copy link
Contributor

It looks like the intent of the code is, if X-Forwarded-Proto is not present, ...

I think the intent is to check that it is not explicitly set to nil in which case it would not be updated.

Why do you think proxy should pass per-existing value?

@willchan
Copy link
Contributor Author

It looks like the intent of the code is, if X-Forwarded-Proto is not present, ...

I think the intent is to check that it is not explicitly set to nil in which case it would not be updated.

Why do you think proxy should pass per-existing value?

It's not a standard HTTP header, so I don't know if I can provide an authoritative reference here, but MDN says: "The X-Forwarded-Proto (XFP) header is a de-facto standard header for identifying the protocol (HTTP or HTTPS) that a client used to connect to your proxy or load balancer." If X-Forwarded-Proto is already present, then presumably there is another proxy in front of httputil.ReverseProxy that set that header, and has better information about the protocol the client used. So I think we would want to respect that.

@thanm
Copy link
Contributor

thanm commented Dec 14, 2022

@neild discussed this bug in the release weekly meeting, any updates? Thanks

@neild
Copy link
Contributor

neild commented Dec 14, 2022

This was added by https://go.dev/cl/407414. The intent is to set X-Forwarded-Proto based on the inbound request that we're forwarding. If an inbound request has an X-Forwarded-Proto header, we have no way to tell whether it should be trusted or not. Setting a header that says we received a request over HTTPS when it actually arrived over HTTP seems wrong.

Unfortunately, the ReverseProxy.Director API doesn't give us any way to both set X-Forwarded-Proto by default and to allow the user to override that decision.

The new ReverseProxy.Rewrite API being added in Go 1.20 makes this simple; a Rewrite func can choose to preserve the X-Forwarded-Proto from the inbound request, overwrite it, or whatever.

The whole point of adding Rewrite is that it's impossible to satisfy everyone with the Director API. For example, whatever we do with X-Forwarded-Proto is going to not be what some people want.

Let's revert the change to set X-Forwarded-Host and X-Forwarded-Proto automatically. DIrector can stay frozen as it is was as of 1.18. Aside from security fixes, future improvements can be limited to Rewrite, which is a better base for adding new features without breaking existing users.

@neild neild self-assigned this Dec 14, 2022
@gopherbot
Copy link

Change https://go.dev/cl/457595 mentions this issue: net/http/httputil: don't add X-Forwarded-{Host,Proto} after invoking Director funcs

@willchan
Copy link
Contributor Author

It sounds like the existing semantics will be preserved, and improvements will be limited to Rewrite. This sounds good to me. I just wanted to chime in on part of the comment below to provide some context from our use case for posterity's sake.

This was added by https://go.dev/cl/407414. The intent is to set X-Forwarded-Proto based on the inbound request that we're forwarding. If an inbound request has an X-Forwarded-Proto header, we have no way to tell whether it should be trusted or not. Setting a header that says we received a request over HTTPS when it actually arrived over HTTP seems wrong.

In our use case, a web client (i.e. browser) talks to a cloud provider frontend (e.g. Google Cloud Load Balancer) which sets X-Forwarded-Proto according to the connection used by the web client. GCLB and Cloudflare both document using X-Forwarded-Proto in this manner, and I suspect others do as well.

While it's generally advisable that the cloud provider frontend communicates with its backend over a secure connection, this connection is distinct from the web client <=> cloud frontend connection, which the cloud frontend communicates the protocol used via X-Forwarded-Proto to its backend.

In our use case, we sometimes will insert yet another reverse proxy between the cloud frontend proxy and our app servers. This reverse proxy uses Go's ReverseProxy. Arguably, we could/should define a separate header to communicate the protocol used for the original web client <=> cloud provider frontend connection, but we wrote it as if it were a transparent proxy. That said, MDN indicates that X-Forwarded-Proto should preserve the original protocol between the client and the load balancer:

"The X-Forwarded-Proto (XFP) header is a de-facto standard header for identifying the protocol (HTTP or HTTPS) that a client used to connect to your proxy or load balancer. Your server access logs contain the protocol used between the server and the load balancer, but not the protocol used between the client and the load balancer. To determine the protocol used between the client and the load balancer, the X-Forwarded-Proto request header can be used."

@AlexanderYastrebov
Copy link
Contributor

and I suspect others do as well.

AWS ALB does the same https://docs.aws.amazon.com/elasticloadbalancing/latest/application/x-forwarded-headers.html#x-forwarded-proto (text looks identical to MDN docs)

@golang golang locked and limited conversation to collaborators Dec 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. release-blocker
Projects
None yet
Development

No branches or pull requests

9 participants