diff --git a/service/internal/config/config.go b/service/internal/config/config.go index de5bdcc..aa35153 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -107,6 +107,15 @@ type PrometheusConfig struct { 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. type Config struct { UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"` @@ -160,6 +169,7 @@ type Config struct { InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"` InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"` Prometheus PrometheusConfig `koanf:"prometheus"` + Security SecurityConfig `koanf:"security"` SaveLogs SaveLogsConfig `koanf:"saveLogs"` DefaultIconForActions string `koanf:"defaultIconForActions"` DefaultIconForDirectories string `koanf:"defaultIconForDirectories"` @@ -268,6 +278,11 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.InsecureAllowDumpJwtClaims = false config.Prometheus.Enabled = 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.DefaultIconForDirectories = "📁" config.DefaultIconForBack = "«" diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 43364f5..6b64438 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -16,6 +16,7 @@ func (cfg *Config) Sanitize() { cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLocalUserPasswords() + cfg.sanitizeSecurityHeaders() // 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. func parsePasswordTemplate(source string) string { t, err := template.New("password").Option("missingkey=error").Parse(source) diff --git a/service/internal/httpservers/frontend.go b/service/internal/httpservers/frontend.go index 9860650..c1c1a1f 100644 --- a/service/internal/httpservers/frontend.go +++ b/service/internal/httpservers/frontend.go @@ -23,6 +23,40 @@ import ( 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) { if cfg.LogDebugOptions.SingleFrontendRequests { 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{ Addr: cfg.ListenAddressSingleHTTPFrontend, - Handler: mux, + Handler: securityHeadersMiddleware(cfg, mux), } log.Fatal(srv.ListenAndServe())