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.
This commit is contained in:
r 2023-10-15 15:53:44 +00:00
parent ed521dd33d
commit 67b13c71ba
3 changed files with 37 additions and 6 deletions

View file

@ -27,6 +27,7 @@ type Settings struct {
AntiDopamineMode bool `json:"adm,omitempty"` AntiDopamineMode bool `json:"adm,omitempty"`
HideUnsupportedNotifs bool `json:"hun,omitempty"` HideUnsupportedNotifs bool `json:"hun,omitempty"`
CSS string `json:"css,omitempty"` CSS string `json:"css,omitempty"`
CSSHash string `json:"cssh,omitempty"`
} }
func NewSettings() *Settings { func NewSettings() *Settings {
@ -43,5 +44,6 @@ func NewSettings() *Settings {
AntiDopamineMode: false, AntiDopamineMode: false,
HideUnsupportedNotifs: false, HideUnsupportedNotifs: false,
CSS: "", CSS: "",
CSSHash: "",
} }
} }

View file

@ -1,6 +1,8 @@
package service package service
import ( import (
"crypto/sha256"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
@ -1014,8 +1016,18 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error)
default: default:
return errInvalidArgument return errInvalidArgument
} }
if len(settings.CSS) > 1<<20 { if len(settings.CSS) > 0 {
return errInvalidArgument 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 c.s.Settings = *settings
return c.setSession(c.s) return c.setSession(c.s)

View file

@ -26,6 +26,16 @@ const (
CSRF 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 { func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
r := mux.NewRouter() r := mux.NewRouter()
@ -58,14 +68,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
}(time.Now()) }(time.Now())
} }
var ct string h := c.w.Header()
switch rt { switch rt {
case HTML: 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: case JSON:
ct = "application/json" h.Set("Content-Type", "application/json")
} }
c.w.Header().Add("Content-Type", ct)
err = c.authenticate(at, s.instance) err = c.authenticate(at, s.instance)
if err != nil { if err != nil {
@ -73,6 +83,13 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
return 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) err = f(c)
if err != nil { if err != nil {
writeError(c, err, rt, req.Method == http.MethodGet) writeError(c, err, rt, req.Method == http.MethodGet)