Переглянути джерело

fix: Set common security headers by default (#903)

James Read 4 місяців тому
батько
коміт
894f3ddc5f

+ 15 - 0
service/internal/config/config.go

@@ -107,6 +107,15 @@ type PrometheusConfig struct {
 	DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
 	DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
 }
 }
 
 
+// SecurityConfig allows users to fine tune the security related HTTP headers.
+type SecurityConfig struct {
+	HeaderContentSecurityPolicy bool   `koanf:"headerContentSecurityPolicy"`
+	ContentSecurityPolicy       string `koanf:"contentSecurityPolicy"`
+	HeaderXContentTypeOptions   bool   `koanf:"headerXContentTypeOptions"`
+	HeaderXFrameOptions         bool   `koanf:"headerXFrameOptions"`
+	XFrameOptions               string `koanf:"xFrameOptions"`
+}
+
 // Config is the global config used through the whole app.
 // Config is the global config used through the whole app.
 type Config struct {
 type Config struct {
 	UseSingleHTTPFrontend           bool                       `koanf:"useSingleHTTPFrontend"`
 	UseSingleHTTPFrontend           bool                       `koanf:"useSingleHTTPFrontend"`
@@ -160,6 +169,7 @@ type Config struct {
 	InsecureAllowDumpActionMap      bool                       `koanf:"insecureAllowDumpActionMap"`
 	InsecureAllowDumpActionMap      bool                       `koanf:"insecureAllowDumpActionMap"`
 	InsecureAllowDumpJwtClaims      bool                       `koanf:"insecureAllowDumpJwtClaims"`
 	InsecureAllowDumpJwtClaims      bool                       `koanf:"insecureAllowDumpJwtClaims"`
 	Prometheus                      PrometheusConfig           `koanf:"prometheus"`
 	Prometheus                      PrometheusConfig           `koanf:"prometheus"`
+	Security                        SecurityConfig             `koanf:"security"`
 	SaveLogs                        SaveLogsConfig             `koanf:"saveLogs"`
 	SaveLogs                        SaveLogsConfig             `koanf:"saveLogs"`
 	DefaultIconForActions           string                     `koanf:"defaultIconForActions"`
 	DefaultIconForActions           string                     `koanf:"defaultIconForActions"`
 	DefaultIconForDirectories       string                     `koanf:"defaultIconForDirectories"`
 	DefaultIconForDirectories       string                     `koanf:"defaultIconForDirectories"`
@@ -268,6 +278,11 @@ func DefaultConfigWithBasePort(basePort int) *Config {
 	config.InsecureAllowDumpJwtClaims = false
 	config.InsecureAllowDumpJwtClaims = false
 	config.Prometheus.Enabled = false
 	config.Prometheus.Enabled = false
 	config.Prometheus.DefaultGoMetrics = false
 	config.Prometheus.DefaultGoMetrics = false
+	config.Security.HeaderContentSecurityPolicy = true
+	config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'"
+	config.Security.HeaderXContentTypeOptions = true
+	config.Security.HeaderXFrameOptions = true
+	config.Security.XFrameOptions = "DENY"
 	config.DefaultIconForActions = "😀"
 	config.DefaultIconForActions = "😀"
 	config.DefaultIconForDirectories = "&#128193"
 	config.DefaultIconForDirectories = "&#128193"
 	config.DefaultIconForBack = "«"
 	config.DefaultIconForBack = "«"

+ 20 - 0
service/internal/config/sanitize.go

@@ -16,6 +16,7 @@ func (cfg *Config) Sanitize() {
 	cfg.sanitizeAuthRequireGuestsToLogin()
 	cfg.sanitizeAuthRequireGuestsToLogin()
 	cfg.sanitizeLogHistoryPageSize()
 	cfg.sanitizeLogHistoryPageSize()
 	cfg.sanitizeLocalUserPasswords()
 	cfg.sanitizeLocalUserPasswords()
+	cfg.sanitizeSecurityHeaders()
 
 
 	// log.Infof("cfg %p", cfg)
 	// log.Infof("cfg %p", cfg)
 
 
@@ -183,6 +184,25 @@ func (cfg *Config) sanitizeLocalUserPasswords() {
 	}
 	}
 }
 }
 
 
+func (cfg *Config) sanitizeSecurityHeaders() {
+	cfg.sanitizeSecurityHeadersCSP()
+	cfg.sanitizeSecurityHeadersXFrameOptions()
+}
+
+func (cfg *Config) sanitizeSecurityHeadersCSP() {
+	if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" {
+		return
+	}
+	cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'"
+}
+
+func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
+	if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" {
+		return
+	}
+	cfg.Security.XFrameOptions = "DENY"
+}
+
 // parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment.
 // parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment.
 func parsePasswordTemplate(source string) string {
 func parsePasswordTemplate(source string) string {
 	t, err := template.New("password").Option("missingkey=error").Parse(source)
 	t, err := template.New("password").Option("missingkey=error").Parse(source)

+ 35 - 1
service/internal/httpservers/frontend.go

@@ -23,6 +23,40 @@ import (
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 )
 )
 
 
+func applySecurityHeaders(cfg *config.Config, w http.ResponseWriter) {
+	applyCSP(cfg, w)
+	applyXContentTypeOptions(cfg, w)
+	applyXFrameOptions(cfg, w)
+}
+
+func applyCSP(cfg *config.Config, w http.ResponseWriter) {
+	if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy == "" {
+		return
+	}
+	w.Header().Set("Content-Security-Policy", cfg.Security.ContentSecurityPolicy)
+}
+
+func applyXContentTypeOptions(cfg *config.Config, w http.ResponseWriter) {
+	if !cfg.Security.HeaderXContentTypeOptions {
+		return
+	}
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+}
+
+func applyXFrameOptions(cfg *config.Config, w http.ResponseWriter) {
+	if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions == "" {
+		return
+	}
+	w.Header().Set("X-Frame-Options", cfg.Security.XFrameOptions)
+}
+
+func securityHeadersMiddleware(cfg *config.Config, next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		applySecurityHeaders(cfg, w)
+		next.ServeHTTP(w, r)
+	})
+}
+
 func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
 func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
 	if cfg.LogDebugOptions.SingleFrontendRequests {
 	if cfg.LogDebugOptions.SingleFrontendRequests {
 		log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL)
 		log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL)
@@ -96,7 +130,7 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
 
 
 	srv := &http.Server{
 	srv := &http.Server{
 		Addr:    cfg.ListenAddressSingleHTTPFrontend,
 		Addr:    cfg.ListenAddressSingleHTTPFrontend,
-		Handler: mux,
+		Handler: securityHeadersMiddleware(cfg, mux),
 	}
 	}
 
 
 	log.Fatal(srv.ListenAndServe())
 	log.Fatal(srv.ListenAndServe())