48 lines
2.3 KiB
Go
48 lines
2.3 KiB
Go
package webui
|
|
|
|
import "net/http"
|
|
|
|
// contentSecurityPolicy — Bootstrap/Bootstrap Icons/Quill are all vendored locally
|
|
// under static/vendor/ (no CDN dependency left anywhere), so this only needs 'self'.
|
|
//
|
|
// frame-ancestors 'self' (not 'none'): the webmail compose popup
|
|
// (webmail_compose_widget.html) legitimately loads /webmail/mail/compose in an
|
|
// <iframe> on the SAME origin — 'none' blocks ALL framing including that, which
|
|
// broke the popup entirely (it's framing, just not the third-party clickjacking
|
|
// kind this header exists to stop). 'self' still blocks any other site from
|
|
// framing this app.
|
|
const contentSecurityPolicy = "default-src 'self'; " +
|
|
"script-src 'self' 'unsafe-inline'; " +
|
|
"style-src 'self' 'unsafe-inline'; " +
|
|
"font-src 'self'; " +
|
|
"img-src 'self' data: https:; " +
|
|
"frame-ancestors 'self'"
|
|
|
|
// SecurityHeaders sets standard hardening headers on every response — wrapped once
|
|
// around the whole app's handler in main.go so admin and webmail routes (and login,
|
|
// static assets, /health) all get it uniformly, rather than duplicating the wrap at
|
|
// multiple mux-registration points.
|
|
//
|
|
// 'unsafe-inline' is required for both script-src and style-src: every template in
|
|
// this codebase uses inline <script>/<style> blocks (no nonce or hash pipeline
|
|
// exists), so a strict CSP would break every page. This still meaningfully narrows
|
|
// the attack surface versus no CSP at all — it blocks loading script/style/fonts
|
|
// from any origin other than this server and jsdelivr, which is what actually
|
|
// matters against a stored-XSS-via-inbound-HTML-mail scenario (the message view
|
|
// sanitizes HTML mail with bluemonday before rendering, but CSP is defense in depth
|
|
// for exactly that class of bug).
|
|
func SecurityHeaders(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := w.Header()
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
// SAMEORIGIN, not DENY: the compose popup frames /webmail/mail/compose in an
|
|
// iframe on this same origin — DENY blocked that too (see
|
|
// contentSecurityPolicy's frame-ancestors comment for the matching CSP fix).
|
|
h.Set("X-Frame-Options", "SAMEORIGIN")
|
|
h.Set("Referrer-Policy", "same-origin")
|
|
h.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
|
h.Set("Content-Security-Policy", contentSecurityPolicy)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|