From 67b13c71baea56eeb15532ca1b1377f6da8d18ac Mon Sep 17 00:00:00 2001 From: r Date: Sun, 15 Oct 2023 15:53:44 +0000 Subject: Use CSP header to restrict resource loading This helps mitigate XSS exploits. Users will have to save the settings again to make the custom CSS work. --- model/session.go | 2 ++ service/service.go | 16 ++++++++++++++-- service/transport.go | 25 +++++++++++++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/model/session.go b/model/session.go index f9e4287..61a409c 100644 --- a/model/session.go +++ b/model/session.go @@ -27,6 +27,7 @@ type Settings struct { AntiDopamineMode bool `json:"adm,omitempty"` HideUnsupportedNotifs bool `json:"hun,omitempty"` CSS string `json:"css,omitempty"` + CSSHash string `json:"cssh,omitempty"` } func NewSettings() *Settings { @@ -43,5 +44,6 @@ func NewSettings() *Settings { AntiDopamineMode: false, HideUnsupportedNotifs: false, CSS: "", + CSSHash: "", } } diff --git a/service/service.go b/service/service.go index 2f87fa3..c925b83 100644 --- a/service/service.go +++ b/service/service.go @@ -1,6 +1,8 @@ package service import ( + "crypto/sha256" + "encoding/base64" "errors" "fmt" "mime/multipart" @@ -1014,8 +1016,18 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) default: return errInvalidArgument } - if len(settings.CSS) > 1<<20 { - return errInvalidArgument + if len(settings.CSS) > 0 { + if len(settings.CSS) > 1<<20 { + return errInvalidArgument + } + // For some reason, browsers convert CRLF to LF before calculating + // the hash of the inline resources. + settings.CSS = strings.ReplaceAll(settings.CSS, "\x0d\x0a", "\x0a") + + h := sha256.Sum256([]byte(settings.CSS)) + settings.CSSHash = base64.StdEncoding.EncodeToString(h[:]) + } else { + settings.CSSHash = "" } c.s.Settings = *settings return c.setSession(c.s) diff --git a/service/transport.go b/service/transport.go index 1182d6c..d032cce 100644 --- a/service/transport.go +++ b/service/transport.go @@ -26,6 +26,16 @@ const ( CSRF ) +const csp = "default-src 'none';" + + " img-src *;" + + " media-src *;" + + " font-src *;" + + " child-src *;" + + " connect-src 'self';" + + " form-action 'self';" + + " script-src 'self';" + + " style-src 'self'" + func NewHandler(s *service, verbose bool, staticDir string) http.Handler { r := mux.NewRouter() @@ -58,14 +68,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { }(time.Now()) } - var ct string + h := c.w.Header() switch rt { case HTML: - ct = "text/html; charset=utf-8" + h.Set("Content-Type", "text/html; charset=utf-8") + h.Set("Content-Security-Policy", csp) case JSON: - ct = "application/json" + h.Set("Content-Type", "application/json") } - c.w.Header().Add("Content-Type", ct) err = c.authenticate(at, s.instance) if err != nil { @@ -73,6 +83,13 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { return } + // Override the CSP header to allow custom CSS + if rt == HTML && len(c.s.Settings.CSS) > 0 && + len(c.s.Settings.CSSHash) > 0 { + v := fmt.Sprintf("%s 'sha256-%s'", csp, c.s.Settings.CSSHash) + h.Set("Content-Security-Policy", v) + } + err = f(c) if err != nil { writeError(c, err, rt, req.Method == http.MethodGet) -- cgit v1.2.3