fix(security): add initial support for HSTS and CSP BE-11311 (#47)

pull/12336/head
andres-portainer 2024-10-21 13:52:11 -03:00 committed by GitHub
parent ac293cda1c
commit 3114d4b5c5
5 changed files with 37 additions and 23 deletions

View File

@ -4,6 +4,9 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
@ -16,8 +19,10 @@ type Handler struct {
// NewHandler creates a handler to serve static files. // NewHandler creates a handler to serve static files.
func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler { func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler {
h := &Handler{ h := &Handler{
Handler: handlers.CompressHandler( Handler: security.MWSecureHeaders(
http.FileServer(http.Dir(assetPublicPath)), handlers.CompressHandler(http.FileServer(http.Dir(assetPublicPath))),
featureflags.IsEnabled("hsts"),
featureflags.IsEnabled("csp"),
), ),
wasInstanceDisabled: wasInstanceDisabled, 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().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) handler.Handler.ServeHTTP(w, r)
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/portainer/portainer/api/apikey" "github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/featureflags"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -42,6 +43,8 @@ type (
jwtService portainer.JWTService jwtService portainer.JWTService
apiKeyService apikey.APIKeyService apiKeyService apikey.APIKeyService
revokedJWT sync.Map revokedJWT sync.Map
hsts bool
csp bool
} }
// RestrictedRequestContext is a data structure containing information // RestrictedRequestContext is a data structure containing information
@ -68,6 +71,8 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
dataStore: dataStore, dataStore: dataStore,
jwtService: jwtService, jwtService: jwtService,
apiKeyService: apiKeyService, apiKeyService: apiKeyService,
hsts: featureflags.IsEnabled("hsts"),
csp: featureflags.IsEnabled("csp"),
} }
go b.cleanUpExpiredJWT() go b.cleanUpExpiredJWT()
@ -78,7 +83,7 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
// PublicAccess defines a security check for public API endpoints. // PublicAccess defines a security check for public API endpoints.
// No authentication is required to access these endpoints. // No authentication is required to access these endpoints.
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler { 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. // 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.CookieAuthLookup,
bouncer.JWTAuthLookup, bouncer.JWTAuthLookup,
}, h) }, h)
h = mwSecureHeaders(h) h = MWSecureHeaders(h, bouncer.hsts, bouncer.csp)
return h return h
} }
@ -517,10 +522,17 @@ func extractAPIKey(r *http.Request) (string, bool) {
return "", false return "", false
} }
// mwSecureHeaders provides secure headers middleware for handlers. // MWSecureHeaders provides secure headers middleware for handlers.
func mwSecureHeaders(next http.Handler) http.Handler { func MWSecureHeaders(next http.Handler, hsts, csp bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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") w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })

View File

@ -1646,7 +1646,7 @@ const (
) )
// List of supported features // List of supported features
var SupportedFeatureFlags = []featureflags.Feature{} var SupportedFeatureFlags = []featureflags.Feature{"hsts", "csp"}
const ( const (
_ AuthenticationMethod = iota _ AuthenticationMethod = iota

View File

@ -10,19 +10,6 @@
<meta http-equiv="pragma" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<base id="base" /> <base id="base" />
<script>
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
if (window.origin == 'http://localhost:49000') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:49000/';
window.ddExtension = true;
} else {
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
}
</script>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>

View File

@ -21,6 +21,18 @@ import { onStartupAngular } from './app';
import { configApp } from './config'; import { configApp } from './config';
import { constantsModule } from './ng-constants'; import { constantsModule } from './ng-constants';
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
if (window.origin == 'http://localhost:49000') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:49000/';
window.ddExtension = true;
} else {
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
}
initFeatureService(Edition[process.env.PORTAINER_EDITION]); initFeatureService(Edition[process.env.PORTAINER_EDITION]);
angular angular