From af61179c2f79c3919ff352b33461ec641bd19d81 Mon Sep 17 00:00:00 2001 From: Patrick O'Doherty Date: Thu, 28 Mar 2024 13:15:01 -0700 Subject: [PATCH] safeweb: add opt-in inline style CSP toggle (#11551) Allow the use of inline styles with safeweb via an opt-in configuration item. This will append `style-src "self" "unsafe-inline"` to the default CSP. The `style-src` directive will be used in lieu of the fallback `default-src "self"` directive. Updates tailscale/corp#8027 Signed-off-by: Patrick O'Doherty --- safeweb/http.go | 21 +++++++++++++++++++-- safeweb/http_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/safeweb/http.go b/safeweb/http.go index 11cc20d5b..8abd169d6 100644 --- a/safeweb/http.go +++ b/safeweb/http.go @@ -26,6 +26,10 @@ // - X-Content-Type-Options header on responses set to "nosniff" to prevent MIME type sniffing attacks. // - Referer-Policy header set to "same-origin" to prevent leaking referrer information to third parties. // +// By default the Content-Security-Policy header will disallow inline styles. +// This can be overridden by setting the CSPAllowInlineStyles field to true in +// the safeweb.Config struct. +// // # API routes // // safeweb inspects the Content-Type header of incoming requests to the API mux @@ -118,6 +122,11 @@ type Config struct { // If this is not provided, the Server will generate a random CSRF secret on // startup. CSRFSecret []byte + + // CSPAllowInlineStyles specifies whether to include `style-src: + // unsafe-inline` in the Content-Security-Policy header to permit the use of + // inline CSS. + CSPAllowInlineStyles bool } func (c *Config) setDefaults() error { @@ -144,6 +153,15 @@ func (c Config) newHandler() http.Handler { // as otherwise the browser will reject the cookie csrfProtect := csrf.Protect(c.CSRFSecret, csrf.Secure(c.SecureContext)) + var csp string + if c.CSPAllowInlineStyles { + csp = defaultCSP + `; style-src 'self' 'unsafe-inline'` + } else { + // if no style-src is provided the browser will fallback to the + // default-src directive which disallows inline styles. + csp = defaultCSP + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, p := c.BrowserMux.Handler(r); p == "" { // disallow x-www-form-urlencoded requests to the API @@ -161,8 +179,7 @@ func (c Config) newHandler() http.Handler { return } - // TODO(@patrickod) consider templating additions to the CSP header. - w.Header().Set("Content-Security-Policy", defaultCSP) + w.Header().Set("Content-Security-Policy", csp) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referer-Policy", "same-origin") csrfProtect(c.BrowserMux).ServeHTTP(w, r) diff --git a/safeweb/http_test.go b/safeweb/http_test.go index 07d921644..8131c2a97 100644 --- a/safeweb/http_test.go +++ b/safeweb/http_test.go @@ -6,6 +6,8 @@ import ( "net/http" "net/http/httptest" + "strconv" + "strings" "testing" "github.com/gorilla/csrf" @@ -364,3 +366,29 @@ func TestRefererPolicy(t *testing.T) { }) } } + +func TestCSPAllowInlineStyles(t *testing.T) { + for _, allow := range []bool{false, true} { + t.Run(strconv.FormatBool(allow), func(t *testing.T) { + h := &http.ServeMux{} + h.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + })) + s, err := NewServer(Config{BrowserMux: h, CSPAllowInlineStyles: allow}) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + s.h.Handler.ServeHTTP(w, req) + resp := w.Result() + + csp := resp.Header.Get("Content-Security-Policy") + allowsStyles := strings.Contains(csp, "style-src 'self' 'unsafe-inline'") + if allowsStyles != allow { + t.Fatalf("CSP inline styles want: %v; got: %v", allow, allowsStyles) + } + }) + } +}