Repository: fleetdm/fleet (Go)

Issue:
Websocket origin header check
## Goal

Introduce a configuration to disable the origin check header during websocket connections.

## Context

Checking the Origin header can be a safe practice when handling WebSocket connections, as it helps prevent cross-site WebSocket hijacking attacks. When a client wants to establish a WebSocket connection with a server, it sends an HTTP request that includes an Origin header indicating the URL of the client web page. The server can check this header to ensure that the client is allowed to connect.

However, there are some cases where disabling the origin header check might be necessary or desirable. Here are some reasons why you might want to do so:

Reverse proxies: If your WebSocket server is behind a reverse proxy, the Origin header may not reflect the client's true origin. In this case, you might need to disable the origin header check or configure your reverse proxy to forward the correct Origin header.

Legacy clients: Older WebSocket clients may not send an Origin header, or may send an incorrect Origin header. If you need to support these clients, you might need to disable the origin header check or add a fallback mechanism.

Testing: During development or testing, you might want to temporarily disable the origin header check to make it easier to test your WebSocket server.

Interoperability: Some WebSocket clients or servers may not implement the Origin header correctly or may have bugs related to it. In these cases, disabling the origin header check might be necessary for interoperability.

## Potential solutions

introduce `websockets_allow_unsafe_origin` configuration that will allow users to disable the origin check.




Fix (patch):
diff --git a/changes/issue-10778-webscokets-unsafe-origin b/changes/issue-10778-webscokets-unsafe-origin
new file mode 100644
index 000000000000..8d870a800ed2
--- /dev/null
+++ b/changes/issue-10778-webscokets-unsafe-origin
@@ -0,0 +1,1 @@
+* add configuration option `websockets_allow_unsafe_origin` to optionally disable the websocket origin check
\ No newline at end of file
diff --git a/docs/Deploying/Configuration.md b/docs/Deploying/Configuration.md
index 6dcda78e50c0..6241c2ccc543 100644
--- a/docs/Deploying/Configuration.md
+++ b/docs/Deploying/Configuration.md
@@ -711,6 +711,23 @@ Turning off keepalives has helped reduce outstanding TCP connections in some dep
   	keepalive: true
   ```
 
+##### server_websockets_allow_unsafe_origin
+
+Controls the servers websocket origin check. If your Fleet server is behind a reverse proxy,
+the Origin header may not reflect the client's true origin. In this case, you might need to 
+disable the origin header (by setting this configuration to `true`) 
+check or configure your reverse proxy to forward the correct Origin header.
+
+Setting to true will disable the origin check.
+
+- Default value: false
+- Environment variable: `FLEET_SERVER_WEBSOCKETS_ALLOW_UNSAFE_ORIGIN`
+- Config file format:
+  ```
+  server:
+  	websockets_allow_unsafe_origin: true
+  ```
+
 ##### Example YAML
 
 ```yaml
diff --git a/server/config/config.go b/server/config/config.go
index b6ceee405d7f..178acd61cefe 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -85,14 +85,15 @@ const (
 
 // ServerConfig defines configs related to the Fleet server
 type ServerConfig struct {
-	Address        string
-	Cert           string
-	Key            string
-	TLS            bool
-	TLSProfile     string `yaml:"tls_compatibility"`
-	URLPrefix      string `yaml:"url_prefix"`
-	Keepalive      bool   `yaml:"keepalive"`
-	SandboxEnabled bool   `yaml:"sandbox_enabled"`
+	Address                     string
+	Cert                        string
+	Key                         string
+	TLS                         bool
+	TLSProfile                  string `yaml:"tls_compatibility"`
+	URLPrefix                   string `yaml:"url_prefix"`
+	Keepalive                   bool   `yaml:"keepalive"`
+	SandboxEnabled              bool   `yaml:"sandbox_enabled"`
+	WebsocketsAllowUnsafeOrigin bool   `yaml:"websockets_allow_unsafe_origin"`
 }
 
 func (s *ServerConfig) DefaultHTTPServer(ctx context.Context, handler http.Handler) *http.Server {
@@ -763,6 +764,7 @@ func (man Manager) addConfigs() {
 		"Controls whether HTTP keep-alives are enabled.")
 	man.addConfigBool("server.sandbox_enabled", false,
 		"When enabled, Fleet limits some features for the Sandbox")
+	man.addConfigBool("server.websockets_allow_unsafe_origin", false, "Disable checking the origin header on websocket connections, this is sometimes necessary when proxies rewrite origin headers between the client and the Fleet webserver")
 
 	// Hide the sandbox flag as we don't want it to be discoverable for users for now
 	sandboxFlag := man.command.PersistentFlags().Lookup(flagNameFromConfigKey("server.sandbox_enabled"))
@@ -1081,14 +1083,15 @@ func (man Manager) LoadConfig() FleetConfig {
 			ReadTimeout:               man.getConfigDuration("redis.read_timeout"),
 		},
 		Server: ServerConfig{
-			Address:        man.getConfigString("server.address"),
-			Cert:           man.getConfigString("server.cert"),
-			Key:            man.getConfigString("server.key"),
-			TLS:            man.getConfigBool("server.tls"),
-			TLSProfile:     man.getConfigTLSProfile(),
-			URLPrefix:      man.getConfigString("server.url_prefix"),
-			Keepalive:      man.getConfigBool("server.keepalive"),
-			SandboxEnabled: man.getConfigBool("server.sandbox_enabled"),
+			Address:                     man.getConfigString("server.address"),
+			Cert:                        man.getConfigString("server.cert"),
+			Key:                         man.getConfigString("server.key"),
+			TLS:                         man.getConfigBool("server.tls"),
+			TLSProfile:                  man.getConfigTLSProfile(),
+			URLPrefix:                   man.getConfigString("server.url_prefix"),
+			Keepalive:                   man.getConfigBool("server.keepalive"),
+			SandboxEnabled:              man.getConfigBool("server.sandbox_enabled"),
+			WebsocketsAllowUnsafeOrigin: man.getConfigBool("server.websockets_allow_unsafe_origin"),
 		},
 		Auth: AuthConfig{
 			BcryptCost:  man.getConfigInt("auth.bcrypt_cost"),
diff --git a/server/service/endpoint_campaigns.go b/server/service/endpoint_campaigns.go
index 28965c3fc6fd..8246f6a3340a 100644
--- a/server/service/endpoint_campaigns.go
+++ b/server/service/endpoint_campaigns.go
@@ -3,6 +3,7 @@ package service
 import (
 	"context"
 	"encoding/json"
+	"github.com/fleetdm/fleet/v4/server/config"
 	"net/http"
 	"regexp"
 	"strings"
@@ -20,11 +21,17 @@ import (
 
 var reVersion = regexp.MustCompile(`\{fleetversion:\(\?:([^\}\)]+)\)\}`)
 
-func makeStreamDistributedQueryCampaignResultsHandler(svc fleet.Service, logger kitlog.Logger) func(string) http.Handler {
+func makeStreamDistributedQueryCampaignResultsHandler(config config.ServerConfig, svc fleet.Service, logger kitlog.Logger) func(string) http.Handler {
 	opt := sockjs.DefaultOptions
 	opt.Websocket = true
 	opt.RawWebsocket = true
 
+	if config.WebsocketsAllowUnsafeOrigin {
+		opt.CheckOrigin = func(r *http.Request) bool {
+			return true
+		}
+	}
+
 	return func(path string) http.Handler {
 		// expand the path's versions (with regex) to all literal paths (no regex),
 		// because sockjs requires the (static, literal) path prefix as argument to
diff --git a/server/service/handler.go b/server/service/handler.go
index a4fef7d6266b..66345e4d56d9 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -568,7 +568,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
 	// endpoint but a raw http.Handler. It uses the NoAuthEndpointer because
 	// authentication is done when the websocket session is established, inside
 	// the handler.
-	ne.UsePathPrefix().PathHandler("GET", "/api/_version_/fleet/results/", makeStreamDistributedQueryCampaignResultsHandler(svc, logger))
+	ne.UsePathPrefix().PathHandler("GET", "/api/_version_/fleet/results/", makeStreamDistributedQueryCampaignResultsHandler(config.Server, svc, logger))
 
 	quota := throttled.RateQuota{MaxRate: throttled.PerHour(10), MaxBurst: 90}
 	limiter := ratelimit.NewMiddleware(limitStore)