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