diff --git a/handler/middleware/cross_site_request_forgery.go b/handler/middleware/cross_site_request_forgery.go index c8ed1a4..9fbca5f 100644 --- a/handler/middleware/cross_site_request_forgery.go +++ b/handler/middleware/cross_site_request_forgery.go @@ -2,11 +2,10 @@ package middleware import ( "fmt" + "net/http" "strings" "me-fit/service" - - "net/http" ) type csrfResponseWriter struct { diff --git a/handler/middleware/logger.go b/handler/middleware/logger.go index 1514114..63d6eb4 100644 --- a/handler/middleware/logger.go +++ b/handler/middleware/logger.go @@ -1,12 +1,12 @@ package middleware import ( - "me-fit/log" - "net/http" "strconv" "time" + "me-fit/log" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) diff --git a/handler/middleware/security_headers.go b/handler/middleware/security_headers.go index e1b2e01..2dce7f3 100644 --- a/handler/middleware/security_headers.go +++ b/handler/middleware/security_headers.go @@ -7,33 +7,26 @@ import ( ) func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Handler { - cspValues := map[string]string{ - "default-src": "'none'", - "script-src": "'self' https://umami.me-fit.eu", - "connect-src": "'self' https://umami.me-fit.eu", - "img-src": "'self'", - "style-src": "'self'", - "form-action": "'self'", - "frame-ancestors": "'none'", - } - - var csp string - for key, value := range cspValues { - csp += key + " " + value + "; " - } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") - w.Header().Set("Content-Security-Policy", csp) + w.Header().Set("Content-Security-Policy", + "default-src 'none';"+ + "script-src 'self' https://umami.me-fit.eu"+ + "connect-src 'self' https://umami.me-fit.eu"+ + "img-src 'self'"+ + "style-src 'self'"+ + "form-action 'self'"+ + "frame-ancestors 'none'", + ) w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") - w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=()") + w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=(), interest-cohort=()") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - w.Header().Set("Permissions-Policy", "interest-cohort=()") w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload") if r.Method == "OPTIONS" { diff --git a/main_test.go b/main_test.go index 1268cfc..ec9b900 100644 --- a/main_test.go +++ b/main_test.go @@ -30,8 +30,83 @@ var ( port atomic.Int32 ) -func TestSecurity(t *testing.T) { +func TestSecurityHeader(t *testing.T) { t.Parallel() + t.Run("should keep caching for static content", func(t *testing.T) { + t.Parallel() + + _, basePath, ctx := setupIntegrationTest(t) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/static/favicon.svg", nil) + assert.Nil(t, err) + + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + cacheControl := resp.Header.Get("Cache-Control") + assert.Equal(t, "", cacheControl) + }) + t.Run("should disable caching for dynamic content", func(t *testing.T) { + t.Parallel() + + _, basePath, ctx := setupIntegrationTest(t) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) + assert.Nil(t, err) + + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + cacheControl := resp.Header.Get("Cache-Control") + assert.Equal(t, "no-cache, no-store, must-revalidate", cacheControl) + }) + t.Run("should include security headers", func(t *testing.T) { + t.Parallel() + + _, basePath, ctx := setupIntegrationTest(t) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath, nil) + assert.Nil(t, err) + + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + value := resp.Header.Get("X-Content-Type-Options") + assert.Equal(t, "nosniff", value) + + value = resp.Header.Get("Access-Control-Allow-Origin") + assert.Equal(t, basePath, value) + + value = resp.Header.Get("Access-Control-Allow-Methods") + assert.Equal(t, "GET, POST, DELETE", value) + + value = resp.Header.Get("Content-Security-Policy") + assert.Equal(t, "default-src 'none';"+ + "script-src 'self' https://umami.me-fit.eu"+ + "connect-src 'self' https://umami.me-fit.eu"+ + "img-src 'self'"+ + "style-src 'self'"+ + "form-action 'self'"+ + "frame-ancestors 'none'", value) + + value = resp.Header.Get("Cross-Origin-Resource-Policy") + assert.Equal(t, "same-origin", value) + + value = resp.Header.Get("Cross-Origin-Opener-Policy") + assert.Equal(t, "same-origin", value) + + value = resp.Header.Get("Cross-Origin-Embedder-Policy") + assert.Equal(t, "require-corp", value) + + value = resp.Header.Get("Permissions-Policy") + assert.Equal(t, "geolocation=(), camera=(), microphone=(), interest-cohort=()", value) + + value = resp.Header.Get("Referrer-Policy") + assert.Equal(t, "strict-origin-when-cross-origin", value) + + value = resp.Header.Get("Strict-Transport-Security") + assert.Equal(t, "max-age=63072000; includeSubDomains; preload", value) + }) } func TestAuth(t *testing.T) {