diff --git a/web/handlers.go b/web/handlers.go index 9cc655eb40..b2d49742ab 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -235,6 +235,11 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge)) } + // Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress + w.Header().Set("Permissions-Policy", "") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Referrer-Policy", "no-referrer") + cloudCSP := "" if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase { cloudCSP = " js.stripe.com/v3" diff --git a/web/handlers_test.go b/web/handlers_test.go index ba49272c49..b5aa35002e 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -59,6 +59,44 @@ func TestHandlerServeHTTPErrors(t *testing.T) { } } +func handlerForServeDefaultSecurityHeaders(c *Context, w http.ResponseWriter, r *http.Request) { +} + +func TestHandlerServeDefaultSecurityHeaders(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + web := New(th.Server) + handler := web.NewHandler(handlerForServeDefaultSecurityHeaders) + + paths := []string{ + "/api/v4/test", // API + "/static/manifest.json", // this should always exist. Static files have their own handler + // Note that the plugin handler isn't tested, also plugins may support arbitrary functionality + } + + for _, path := range paths { + request := httptest.NewRequest("GET", path, nil) + + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + // header.Get returns a "" also if the header doesn't exist so we check that there is at least + // one Permissions-Policy header and their value is "". We check with .Values() as it canonicalizes + // the key. + permissionsPolicyHeader := response.Header().Get("Permissions-Policy") + permissionsPolicyHeaderValues := response.Header().Values("Permissions-Policy") + + contentTypeOptionsHeader := response.Header().Get("X-Content-Type-Options") + referrerPolicyHeader := response.Header().Get("Referrer-Policy") + + assert.NotEqualf(t, 0, len(permissionsPolicyHeaderValues), "Permissions-Policy header doesn't exist") + assert.Equal(t, "", permissionsPolicyHeader, "Permissions-Policy is not empty") + assert.Equal(t, "nosniff", contentTypeOptionsHeader) + assert.Equal(t, "no-referrer", referrerPolicyHeader) + } +} + func handlerForHTTPSecureTransport(c *Context, w http.ResponseWriter, r *http.Request) { } diff --git a/web/static.go b/web/static.go index a694f747db..5e856e267e 100644 --- a/web/static.go +++ b/web/static.go @@ -87,6 +87,11 @@ func staticFilesHandler(handler http.Handler) http.Handler { w.Header().Set("Cache-Control", "max-age=31556926, public") } + // Hardcoded sensible default values for these security headers. Feel free to override in proxy or ingress + w.Header().Set("Permissions-Policy", "") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Referrer-Policy", "no-referrer") + if strings.HasSuffix(r.URL.Path, "/") { http.NotFound(w, r) return