fix: WebSocket proxy to respect PassHostHeader setting (#3290)

* Fix WebSocket proxy to respect PassHostHeader setting

When PassHostHeader is set to false, the regular HTTP proxy correctly
sets the Host header to the upstream backend URL. However, the WebSocket
proxy was not respecting this setting, causing WebSocket connections to
fail when backend services validate the Host header.

This commit:
- Adds passHostHeader parameter to newWebSocketReverseProxy()
- Applies setProxyUpstreamHostHeader() when PassHostHeader=false
- Ensures consistent behavior between HTTP and WebSocket proxies

Fixes #3288

Signed-off-by: Pascal Schmiel <pascal.schmiel@gmail.com>

* chore(): add tests, update changelog

Signed-off-by: Pascal Schmiel <pascal.schmiel@gmail.com>

---------

Signed-off-by: Pascal Schmiel <pascal.schmiel@gmail.com>
This commit is contained in:
Pascal 2026-01-16 20:30:16 +01:00 committed by GitHub
parent 3c22bc7877
commit 1d6721f7ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 6 deletions

View File

@ -8,6 +8,7 @@
## Changes since v7.13.0
- [#3290](https://github.com/oauth2-proxy/oauth2-proxy/pull/3290) fix: WebSocket proxy to respect PassHostHeader setting (@UnsignedLong)
- [#3197](https://github.com/oauth2-proxy/oauth2-proxy/pull/3197) fix: NewRemoteKeySet is not using DefaultHTTPClient (@rsrdesarrollo / @tuunit)
- [#3292](https://github.com/oauth2-proxy/oauth2-proxy/pull/3292) chore(deps): upgrade gomod and bump to golang v1.25.5 (@tuunit)
- [#3304](https://github.com/oauth2-proxy/oauth2-proxy/pull/3304) fix: added conditional so default is not always set and env vars are honored fixes 3303 (@pixeldrew)

View File

@ -55,7 +55,7 @@ func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *option
// Set up a WebSocket proxy if required
var wsProxy http.Handler
if ptr.Deref(upstream.ProxyWebSockets, options.DefaultUpstreamProxyWebSockets) {
wsProxy = newWebSocketReverseProxy(u, upstream.InsecureSkipTLSVerify)
wsProxy = newWebSocketReverseProxy(u, upstream.InsecureSkipTLSVerify, upstream.PassHostHeader)
}
var auth hmacauth.HmacAuth
@ -201,7 +201,7 @@ func setProxyDirector(proxy *httputil.ReverseProxy) {
}
// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool) http.Handler {
func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool, passHostHeader *bool) http.Handler {
wsProxy := httputil.NewSingleHostReverseProxy(u)
// Inherit default transport options from Go's stdlib
@ -215,5 +215,10 @@ func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool) http.Handler {
// Apply the customized transport to our proxy before returning it
wsProxy.Transport = transport
// Set upstream host header if PassHostHeader is false (same as regular HTTP proxy)
if !ptr.Deref(passHostHeader, options.DefaultUpstreamPassHostHeader) {
setProxyUpstreamHostHeader(wsProxy, u)
}
return wsProxy
}

View File

@ -519,10 +519,11 @@ var _ = Describe("HTTP Upstream Suite", func() {
Expect(websocket.Message.Send(ws, []byte(message))).To(Succeed())
var response testWebSocketResponse
Expect(websocket.JSON.Receive(ws, &response)).To(Succeed())
Expect(response).To(Equal(testWebSocketResponse{
Message: message,
Origin: origin,
}))
// When PassHostHeader=true (default), the Host should be the client's original request host
Expect(response.Message).To(Equal(message))
Expect(response.Origin).To(Equal(origin))
Expect(response.Host).To(Equal(proxyURL.Host))
})
It("will proxy HTTP requests", func() {
@ -530,5 +531,45 @@ var _ = Describe("HTTP Upstream Suite", func() {
Expect(err).ToNot(HaveOccurred())
Expect(response.StatusCode).To(Equal(200))
})
It("will proxy websockets respecting PassHostHeader=false", func() {
// Create a new proxy server with PassHostHeader=false
flush := 1 * time.Second
timeout := options.DefaultUpstreamTimeout
upstream := options.Upstream{
ID: "websocketProxyNoPassHost",
PassHostHeader: ptr.To(false),
ProxyWebSockets: ptr.To(true),
InsecureSkipTLSVerify: ptr.To(false),
FlushInterval: &flush,
Timeout: &timeout,
}
u, err := url.Parse(serverAddr)
Expect(err).ToNot(HaveOccurred())
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
noPassHostServer := httptest.NewServer(middleware.NewScope(false, "X-Request-Id")(handler))
defer noPassHostServer.Close()
origin := "http://example.localhost"
message := "Hello, world!"
proxyURL, err := url.Parse(fmt.Sprintf("http://%s", noPassHostServer.Listener.Addr().String()))
Expect(err).ToNot(HaveOccurred())
wsAddr := fmt.Sprintf("ws://%s/", proxyURL.Host)
ws, err := websocket.Dial(wsAddr, "", origin)
Expect(err).ToNot(HaveOccurred())
Expect(websocket.Message.Send(ws, []byte(message))).To(Succeed())
var response testWebSocketResponse
Expect(websocket.JSON.Receive(ws, &response)).To(Succeed())
// When PassHostHeader=false, the Host should be the upstream server address
Expect(response.Host).To(Equal(u.Host))
Expect(response.Message).To(Equal(message))
Expect(response.Origin).To(Equal(origin))
})
})
})

View File

@ -96,6 +96,7 @@ type testHTTPRequest struct {
type testWebSocketResponse struct {
Message string
Origin string
Host string
}
type testHTTPUpstream struct{}
@ -138,6 +139,7 @@ func (t *testHTTPUpstream) websocketHandler() http.Handler {
wsResponse := testWebSocketResponse{
Message: string(data),
Origin: ws.Request().Header.Get("Origin"),
Host: ws.Request().Host,
}
err = websocket.JSON.Send(ws, wsResponse)
if err != nil {