mirror of https://github.com/portainer/portainer
fix(security): add initial support for HSTS and CSP BE-11311 (#47)
parent
ac293cda1c
commit
3114d4b5c5
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]>
|
||||||
|
|
12
app/index.js
12
app/index.js
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue