From 3114d4b5c5f58f8941be1cb7ceb5ed3e68989644 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:52:11 -0300 Subject: [PATCH] fix(security): add initial support for HSTS and CSP BE-11311 (#47) --- api/http/handler/file/handler.go | 11 +++++++---- api/http/security/bouncer.go | 22 +++++++++++++++++----- api/portainer.go | 2 +- app/index.html | 13 ------------- app/index.js | 12 ++++++++++++ 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/api/http/handler/file/handler.go b/api/http/handler/file/handler.go index 5af91fb9f..ec4f66b70 100644 --- a/api/http/handler/file/handler.go +++ b/api/http/handler/file/handler.go @@ -4,6 +4,9 @@ import ( "net/http" "strings" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/pkg/featureflags" + "github.com/gorilla/handlers" ) @@ -16,8 +19,10 @@ type Handler struct { // NewHandler creates a handler to serve static files. func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler { h := &Handler{ - Handler: handlers.CompressHandler( - http.FileServer(http.Dir(assetPublicPath)), + Handler: security.MWSecureHeaders( + handlers.CompressHandler(http.FileServer(http.Dir(assetPublicPath))), + featureflags.IsEnabled("hsts"), + featureflags.IsEnabled("csp"), ), wasInstanceDisabled: wasInstanceDisabled, } @@ -53,7 +58,5 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") } - w.Header().Add("X-XSS-Protection", "1; mode=block") - w.Header().Add("X-Content-Type-Options", "nosniff") handler.Handler.ServeHTTP(w, r) } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 7ac8e2367..00f69e328 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/portainer/api/apikey" "github.com/portainer/portainer/api/dataservices" httperrors "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/pkg/featureflags" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/pkg/errors" @@ -42,6 +43,8 @@ type ( jwtService portainer.JWTService apiKeyService apikey.APIKeyService revokedJWT sync.Map + hsts bool + csp bool } // RestrictedRequestContext is a data structure containing information @@ -68,6 +71,8 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW dataStore: dataStore, jwtService: jwtService, apiKeyService: apiKeyService, + hsts: featureflags.IsEnabled("hsts"), + csp: featureflags.IsEnabled("csp"), } go b.cleanUpExpiredJWT() @@ -78,7 +83,7 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW // PublicAccess defines a security check for public API endpoints. // No authentication is required to access these endpoints. func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler { - return mwSecureHeaders(h) + return MWSecureHeaders(h, bouncer.hsts, bouncer.csp) } // AdminAccess defines a security check for API endpoints that require an authorization check. @@ -211,7 +216,7 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler bouncer.CookieAuthLookup, bouncer.JWTAuthLookup, }, h) - h = mwSecureHeaders(h) + h = MWSecureHeaders(h, bouncer.hsts, bouncer.csp) return h } @@ -517,10 +522,17 @@ func extractAPIKey(r *http.Request) (string, bool) { return "", false } -// mwSecureHeaders provides secure headers middleware for handlers. -func mwSecureHeaders(next http.Handler) http.Handler { +// MWSecureHeaders provides secure headers middleware for handlers. +func MWSecureHeaders(next http.Handler, hsts, csp bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-XSS-Protection", "1; mode=block") + if hsts { + w.Header().Set("Strict-Transport-Security", "max-age=31536000") // 365 days + } + + if csp { + w.Header().Set("Content-Security-Policy", "script-src 'self' cdn.matomo.cloud") + } + w.Header().Set("X-Content-Type-Options", "nosniff") next.ServeHTTP(w, r) }) diff --git a/api/portainer.go b/api/portainer.go index b3b3579ea..17bd6cf2d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1646,7 +1646,7 @@ const ( ) // List of supported features -var SupportedFeatureFlags = []featureflags.Feature{} +var SupportedFeatureFlags = []featureflags.Feature{"hsts", "csp"} const ( _ AuthenticationMethod = iota diff --git a/app/index.html b/app/index.html index 5a21bd71b..370070b48 100644 --- a/app/index.html +++ b/app/index.html @@ -10,19 +10,6 @@ -