diff --git a/config/config.go b/config/config.go index 9d443b9cb..c2fdaa660 100644 --- a/config/config.go +++ b/config/config.go @@ -205,6 +205,9 @@ func VerifyConfig() error { cssFontRestriction = "'self'" } + // The default Content Security Policy is updates on every application page load to replace script-src 'self' + // with a random nonce ID to prevent XSS. This applies to the Mailpit app & API. + // See server.middleWareFunc() ContentSecurityPolicy = fmt.Sprintf("default-src 'self'; script-src 'self'; style-src %s 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src %s data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';", cssFontRestriction, cssFontRestriction, ) diff --git a/package-lock.json b/package-lock.json index 6788fbf77..fde6e5c08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "bootstrap5-tags": "^1.6.1", "color-hash": "^2.0.2", "dayjs": "^1.11.10", + "dompurify": "^3.1.6", "ical.js": "^2.0.1", "modern-screenshot": "^4.4.30", "prismjs": "^1.29.0", @@ -1417,6 +1418,11 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", diff --git a/package.json b/package.json index d48725c6d..be6f5a10c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "bootstrap5-tags": "^1.6.1", "color-hash": "^2.0.2", "dayjs": "^1.11.10", + "dompurify": "^3.1.6", "ical.js": "^2.0.1", "modern-screenshot": "^4.4.30", "prismjs": "^1.29.0", diff --git a/server/server.go b/server/server.go index aee94d59d..55dfae362 100644 --- a/server/server.go +++ b/server/server.go @@ -25,6 +25,7 @@ import ( "github.com/axllent/mailpit/server/pop3" "github.com/axllent/mailpit/server/websockets" "github.com/gorilla/mux" + "github.com/lithammer/shortuuid/v4" ) //go:embed ui @@ -75,11 +76,11 @@ func Listen() { } // UI shortcut - r.HandleFunc(config.Webroot+"view/latest", handlers.RedirectToLatestMessage).Methods("GET") + r.HandleFunc(config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage)).Methods("GET") // frontend testing - r.HandleFunc(config.Webroot+"view/{id}.html", handlers.GetMessageHTML).Methods("GET") - r.HandleFunc(config.Webroot+"view/{id}.txt", handlers.GetMessageText).Methods("GET") + r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(handlers.GetMessageHTML)).Methods("GET") + r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(handlers.GetMessageText)).Methods("GET") // web UI via virtual index.html r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET") @@ -179,7 +180,21 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) { func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Referrer-Policy", "no-referrer") - w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy) + + // generate a new random nonce on every request + randomNonce := shortuuid.New() + // header used to pass nonce through to function + r.Header.Set("mp-nonce", randomNonce) + + // Prevent JavaScript XSS by adding a nonce for script-src + cspHeader := strings.Replace( + config.ContentSecurityPolicy, + "script-src 'self';", + fmt.Sprintf("script-src 'nonce-%s';", randomNonce), + 1, + ) + + w.Header().Set("Content-Security-Policy", cspHeader) if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") { w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin) @@ -281,7 +296,7 @@ func swaggerBasePath(w http.ResponseWriter, _ *http.Request) { } // Just returns the default HTML template -func index(w http.ResponseWriter, _ *http.Request) { +func index(w http.ResponseWriter, r *http.Request) { var h = ` @@ -298,10 +313,12 @@ func index(w http.ResponseWriter, _ *http.Request) {