From 19fb9675a43768e16378893f7b3a172e8f2a0514 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 15 Dec 2023 21:21:58 +0200 Subject: [PATCH] Add CONTENT_SECURITY_POLICY --- internal/config/config_test.go | 18 ++++++++++++++++++ internal/config/options.go | 9 +++++++++ internal/config/parser.go | 2 ++ internal/template/templates/common/layout.html | 10 +++++++--- internal/ui/view/view.go | 1 + miniflux.1 | 9 +++++++++ 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bcf58da3..b86a53e4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2109,3 +2109,21 @@ func TestParseConfigDumpOutput(t *testing.T) { t.Fatal(err) } } + +func TestContentSecurityPolicy(t *testing.T) { + os.Clearenv() + os.Setenv("CONTENT_SECURITY_POLICY", "default-src 'self' fonts.googleapis.com fonts.gstatic.com; img-src * data:; media-src *; frame-src *; style-src 'self' fonts.googleapis.com fonts.gstatic.com 'nonce-%s'") + + parser := NewParser() + opts, err := parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + expected := "default-src 'self' fonts.googleapis.com fonts.gstatic.com; img-src * data:; media-src *; frame-src *; style-src 'self' fonts.googleapis.com fonts.gstatic.com 'nonce-%s'" + result := opts.ContentSecurityPolicy() + + if result != expected { + t.Fatalf(`Unexpected CONTENT_SECURITY_POLICY value, got %v instead of %v`, result, expected) + } +} diff --git a/internal/config/options.go b/internal/config/options.go index 89bff536..744af88b 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -85,6 +85,7 @@ const ( defaultWatchdog = true defaultInvidiousInstance = "yewtu.be" defaultWebAuthn = false + defaultContentSecurityPolicy = "default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-%s'; require-trusted-types-for 'script'; trusted-types ttpolicy;" ) var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)" @@ -169,6 +170,7 @@ type Options struct { invidiousInstance string mediaProxyPrivateKey []byte webAuthn bool + contentSecurityPolicy string } // NewOptions returns Options with default values. @@ -244,6 +246,7 @@ func NewOptions() *Options { invidiousInstance: defaultInvidiousInstance, mediaProxyPrivateKey: crypto.GenerateRandomBytes(16), webAuthn: defaultWebAuthn, + contentSecurityPolicy: defaultContentSecurityPolicy, } } @@ -620,6 +623,11 @@ func (o *Options) FilterEntryMaxAgeDays() int { return o.filterEntryMaxAgeDays } +// ContentSecurityPolicy returns value for Content-Security-Policy meta tag. +func (o *Options) ContentSecurityPolicy() string { + return o.contentSecurityPolicy +} + // SortedOptions returns options as a list of key value pairs, sorted by keys. func (o *Options) SortedOptions(redactSecret bool) []*Option { var keyValues = map[string]interface{}{ @@ -697,6 +705,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option { "WORKER_POOL_SIZE": o.workerPoolSize, "YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride, "WEBAUTHN": o.webAuthn, + "CONTENT_SECURITY_POLICY": o.contentSecurityPolicy, } keys := make([]string, 0, len(keyValues)) diff --git a/internal/config/parser.go b/internal/config/parser.go index 24704710..6ed0be8e 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -271,6 +271,8 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance) case "WEBAUTHN": p.opts.webAuthn = parseBool(value, defaultWebAuthn) + case "CONTENT_SECURITY_POLICY": + p.opts.contentSecurityPolicy = parseString(value, defaultContentSecurityPolicy) } } diff --git a/internal/template/templates/common/layout.html b/internal/template/templates/common/layout.html index 19019c1e..728f26b5 100644 --- a/internal/template/templates/common/layout.html +++ b/internal/template/templates/common/layout.html @@ -36,8 +36,13 @@ {{ if and .user .user.Stylesheet }} {{ $stylesheetNonce := nonce }} - - + {{ $containsNonce := contains .contentSecurityPolicy "nonce-%s" }} + {{ if $containsNonce }} + {{ noescape ( printf "" (printf .contentSecurityPolicy $stylesheetNonce ) ) }} + {{ else }} + {{ noescape ( printf "" .contentSecurityPolicy ) }} + {{ end }} + {{ else }} {{ end }} @@ -58,7 +63,6 @@ data-webauthn-delete-all-url="{{ route "webauthnDeleteAll" }}" {{ end }} {{ if .user }}{{ if not .user.KeyboardShortcuts }}data-disable-keyboard-shortcuts="true"{{ end }}{{ end }}> - {{ if .user }} {{ t "skip_to_content" }}
diff --git a/internal/ui/view/view.go b/internal/ui/view/view.go index 742bb3a9..a0480388 100644 --- a/internal/ui/view/view.go +++ b/internal/ui/view/view.go @@ -46,5 +46,6 @@ func New(tpl *template.Engine, r *http.Request, sess *session.Session) *View { "sw_js_checksum": static.JavascriptBundleChecksums["service-worker"], "webauthn_js_checksum": static.JavascriptBundleChecksums["webauthn"], "webAuthnEnabled": config.Opts.WebAuthn(), + "contentSecurityPolicy": config.Opts.ContentSecurityPolicy(), }} } diff --git a/miniflux.1 b/miniflux.1 index 67132a5d..2cfd37b7 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -534,6 +534,15 @@ Default is 16 workers\&. YouTube URL which will be used for embeds\&. .br Default is https://www.youtube-nocookie.com/embed/\&. +.TP +.B CONTENT_SECURITY_POLICY +Set custom value for Content-Security-Policy meta tag. Used when custom CSS is applied. +.br +It may contain "nonce-%s", where nonce will be placed\&. +.br +Default is "default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-%s'; require-trusted-types-for 'script'; trusted-types ttpolicy;"\&. +.TP + .SH AUTHORS .P Miniflux is written and maintained by Fr\['e]d\['e]ric Guillot\&.