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 <patrick@tailscale.com>
This commit is contained in:
Patrick O'Doherty 2024-03-28 13:15:01 -07:00 committed by GitHub
parent b0941b79d6
commit af61179c2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 2 deletions

View File

@ -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)

View File

@ -6,6 +6,8 @@ package safeweb
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)
}
})
}
}