From e44f00d0c9811a39d35dcb785dba84a17269052e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=84=B6?= Date: Mon, 15 Jun 2026 12:58:55 +0800 Subject: [PATCH 1/3] fix(ingress): forward all headers in WebSocket proxy instead of hardcoded whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WebSocket proxy only forwarded Origin, Sec-WebSocket-Protocol, and Cookie headers to the backend, silently dropping all others. This caused execd to reject PTY WebSocket connections with 401 because the X-EXECD-ACCESS-TOKEN header never reached it. Replace the whitelist with a blacklist that skips only hop-by-hop headers (RFC 7230 §6.1) and WebSocket handshake headers managed by the dialer, matching httputil.ReverseProxy behavior on the HTTP path. Closes #1050 Co-Authored-By: Claude Opus 4.6 --- components/ingress/pkg/proxy/header.go | 23 ++++++++++++++++++----- components/ingress/pkg/proxy/websocket.go | 22 ++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/components/ingress/pkg/proxy/header.go b/components/ingress/pkg/proxy/header.go index 9105683cd..b32f08b4a 100644 --- a/components/ingress/pkg/proxy/header.go +++ b/components/ingress/pkg/proxy/header.go @@ -29,9 +29,22 @@ var ( AccessControlAllowOrigin = http.CanonicalHeaderKey("Access-Control-Allow-Origin") ReverseProxyServerPowerBy = http.CanonicalHeaderKey("Reverse-Proxy-Server-PowerBy") - SecWebSocketProtocol = http.CanonicalHeaderKey("Sec-WebSocket-Protocol") - Cookie = http.CanonicalHeaderKey("Cookie") - SetCookie = http.CanonicalHeaderKey("Set-Cookie") - Host = http.CanonicalHeaderKey("Host") - Origin = http.CanonicalHeaderKey("Origin") + SecWebSocketProtocol = http.CanonicalHeaderKey("Sec-WebSocket-Protocol") + SecWebSocketKey = http.CanonicalHeaderKey("Sec-WebSocket-Key") + SecWebSocketVersion = http.CanonicalHeaderKey("Sec-WebSocket-Version") + SecWebSocketExtensions = http.CanonicalHeaderKey("Sec-WebSocket-Extensions") + Cookie = http.CanonicalHeaderKey("Cookie") + SetCookie = http.CanonicalHeaderKey("Set-Cookie") + Host = http.CanonicalHeaderKey("Host") + Origin = http.CanonicalHeaderKey("Origin") + + // Hop-by-hop headers per RFC 7230 §6.1 — must not be forwarded by proxies. + HopByHopConnection = http.CanonicalHeaderKey("Connection") + HopByHopKeepAlive = http.CanonicalHeaderKey("Keep-Alive") + HopByHopProxyAuth = http.CanonicalHeaderKey("Proxy-Authenticate") + HopByHopProxyAuthz = http.CanonicalHeaderKey("Proxy-Authorization") + HopByHopTE = http.CanonicalHeaderKey("TE") + HopByHopTrailer = http.CanonicalHeaderKey("Trailer") + HopByHopTransferEncoding = http.CanonicalHeaderKey("Transfer-Encoding") + HopByHopUpgrade = http.CanonicalHeaderKey("Upgrade") ) diff --git a/components/ingress/pkg/proxy/websocket.go b/components/ingress/pkg/proxy/websocket.go index 7d14d7ad8..f22fef1f3 100644 --- a/components/ingress/pkg/proxy/websocket.go +++ b/components/ingress/pkg/proxy/websocket.go @@ -100,17 +100,19 @@ func (w *WebSocketProxy) ServeHTTP(rw http.ResponseWriter, r *http.Request) { dialer = defaultWebSocketDialer } - // Pass headers from the incoming request to the dialer to forward them to - // the final destinations. + // Forward all incoming headers to the backend except hop-by-hop headers + // (RFC 7230 §6.1) and WebSocket handshake headers managed by the dialer. requestHeader := http.Header{} - if origin := r.Header.Get(Origin); origin != "" { - requestHeader.Add(Origin, origin) - } - for _, prot := range r.Header[SecWebSocketProtocol] { - requestHeader.Add(SecWebSocketProtocol, prot) - } - for _, cokiee := range r.Header[Cookie] { - requestHeader.Add(Cookie, cokiee) + for key, values := range r.Header { + switch key { + case HopByHopConnection, HopByHopKeepAlive, HopByHopProxyAuth, HopByHopProxyAuthz, + HopByHopTE, HopByHopTrailer, HopByHopTransferEncoding, HopByHopUpgrade, + SecWebSocketKey, SecWebSocketVersion, SecWebSocketExtensions: + continue + } + for _, v := range values { + requestHeader.Add(key, v) + } } if r.Host != "" { requestHeader.Set(Host, r.Host) From 56539f177d0aaec7ae7285913539a4dd7f74143e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=84=B6?= Date: Mon, 15 Jun 2026 13:01:08 +0800 Subject: [PATCH 2/3] style(ingress): fix gofmt alignment in header.go Co-Authored-By: Claude Opus 4.6 --- components/ingress/pkg/proxy/header.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/ingress/pkg/proxy/header.go b/components/ingress/pkg/proxy/header.go index b32f08b4a..e768e7448 100644 --- a/components/ingress/pkg/proxy/header.go +++ b/components/ingress/pkg/proxy/header.go @@ -29,14 +29,14 @@ var ( AccessControlAllowOrigin = http.CanonicalHeaderKey("Access-Control-Allow-Origin") ReverseProxyServerPowerBy = http.CanonicalHeaderKey("Reverse-Proxy-Server-PowerBy") - SecWebSocketProtocol = http.CanonicalHeaderKey("Sec-WebSocket-Protocol") - SecWebSocketKey = http.CanonicalHeaderKey("Sec-WebSocket-Key") - SecWebSocketVersion = http.CanonicalHeaderKey("Sec-WebSocket-Version") - SecWebSocketExtensions = http.CanonicalHeaderKey("Sec-WebSocket-Extensions") - Cookie = http.CanonicalHeaderKey("Cookie") - SetCookie = http.CanonicalHeaderKey("Set-Cookie") - Host = http.CanonicalHeaderKey("Host") - Origin = http.CanonicalHeaderKey("Origin") + SecWebSocketProtocol = http.CanonicalHeaderKey("Sec-WebSocket-Protocol") + SecWebSocketKey = http.CanonicalHeaderKey("Sec-WebSocket-Key") + SecWebSocketVersion = http.CanonicalHeaderKey("Sec-WebSocket-Version") + SecWebSocketExtensions = http.CanonicalHeaderKey("Sec-WebSocket-Extensions") + Cookie = http.CanonicalHeaderKey("Cookie") + SetCookie = http.CanonicalHeaderKey("Set-Cookie") + Host = http.CanonicalHeaderKey("Host") + Origin = http.CanonicalHeaderKey("Origin") // Hop-by-hop headers per RFC 7230 §6.1 — must not be forwarded by proxies. HopByHopConnection = http.CanonicalHeaderKey("Connection") From 12ac3d1907573d581637adee5f3c20435f1b09f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=84=B6?= Date: Mon, 15 Jun 2026 14:02:58 +0800 Subject: [PATCH 3/3] fix(ingress): also strip headers named by Connection tokens per RFC 7230 Parse Connection header values and skip any header listed as a connection token, in addition to the fixed hop-by-hop set. Co-Authored-By: Claude Opus 4.6 --- components/ingress/pkg/proxy/websocket.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/ingress/pkg/proxy/websocket.go b/components/ingress/pkg/proxy/websocket.go index f22fef1f3..eb881d25f 100644 --- a/components/ingress/pkg/proxy/websocket.go +++ b/components/ingress/pkg/proxy/websocket.go @@ -102,6 +102,15 @@ func (w *WebSocketProxy) ServeHTTP(rw http.ResponseWriter, r *http.Request) { // Forward all incoming headers to the backend except hop-by-hop headers // (RFC 7230 §6.1) and WebSocket handshake headers managed by the dialer. + // Per RFC 7230, also strip any header named by Connection tokens. + connTokens := map[string]bool{} + for _, v := range r.Header[HopByHopConnection] { + for _, token := range strings.Split(v, ",") { + if h := http.CanonicalHeaderKey(strings.TrimSpace(token)); h != "" { + connTokens[h] = true + } + } + } requestHeader := http.Header{} for key, values := range r.Header { switch key { @@ -110,6 +119,9 @@ func (w *WebSocketProxy) ServeHTTP(rw http.ResponseWriter, r *http.Request) { SecWebSocketKey, SecWebSocketVersion, SecWebSocketExtensions: continue } + if connTokens[key] { + continue + } for _, v := range values { requestHeader.Add(key, v) }