Files

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)
})
}