diff --git a/internal/middleware/websocket_origin.go b/internal/middleware/websocket_origin.go index a31d7aa7..8866f68c 100644 --- a/internal/middleware/websocket_origin.go +++ b/internal/middleware/websocket_origin.go @@ -10,11 +10,17 @@ import ( ) // CheckWebSocketOrigin validates browser origins for WebSocket upgrade requests. -// Non-browser requests are only allowed for trusted node-to-node traffic. +// Trusted node-to-node traffic (via X-Node-Secret) is always allowed, +// regardless of the Origin header, because proxied requests carry the +// browser's original Origin which won't match the downstream node's host. func CheckWebSocketOrigin(r *http.Request) bool { + if isTrustedNodeRequest(r) { + return true + } + origin := strings.TrimSpace(r.Header.Get("Origin")) if origin == "" { - return isTrustedNodeRequest(r) + return false } if requestOrigin, ok := getRequestOrigin(r); ok && sameOrigin(origin, requestOrigin) { diff --git a/internal/middleware/websocket_origin_test.go b/internal/middleware/websocket_origin_test.go index a5ed7db0..a8f11f3a 100644 --- a/internal/middleware/websocket_origin_test.go +++ b/internal/middleware/websocket_origin_test.go @@ -64,6 +64,20 @@ func TestCheckWebSocketOrigin(t *testing.T) { assert.True(t, CheckWebSocketOrigin(req)) }) + t.Run("allows node secret requests with cross-origin (proxy scenario)", func(t *testing.T) { + settings.HTTPSettings.WebSocketTrustedOrigins = nil + settings.NodeSettings.Secret = "node-secret" + + // Simulates master node proxying WS to child node: + // Origin is the browser's master domain, but X-Node-Secret proves it's a trusted proxy. + req := httptest.NewRequest("GET", "http://127.0.0.1/ws", nil) + req.Host = "child-node:9000" + req.Header.Set("Origin", "https://master.example.com") + req.Header.Set("X-Node-Secret", "node-secret") + + assert.True(t, CheckWebSocketOrigin(req)) + }) + t.Run("rejects cross site requests", func(t *testing.T) { settings.HTTPSettings.WebSocketTrustedOrigins = nil settings.NodeSettings.Secret = ""