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) + } + }) + } +}