1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-09-15 18:57:04 +00:00
This commit is contained in:
Julien Voisin 2025-09-14 11:25:56 -07:00 committed by GitHub
commit ef564b39c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 11 deletions

View file

@ -33,6 +33,7 @@ type funcMap struct {
func (f *funcMap) Map() template.FuncMap { func (f *funcMap) Map() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"contains": strings.Contains, "contains": strings.Contains,
"csp": csp,
"startsWith": strings.HasPrefix, "startsWith": strings.HasPrefix,
"formatFileSize": formatFileSize, "formatFileSize": formatFileSize,
"dict": dict, "dict": dict,
@ -116,6 +117,42 @@ func (f *funcMap) Map() template.FuncMap {
} }
} }
func csp(user *model.User, nonce string) string {
order := [...]string{"default-src", "img-src", "media-src", "frame-src", "style-src", "script-src", "font-src", "require-trusted-types-for", "trusted-types"}
policies := map[string]string{
"default-src": "'none'",
"img-src": "* data:",
"media-src": "*",
"frame-src": "*",
"style-src": "'nonce-" + nonce + "'",
"script-src": "'nonce-" + nonce + "' 'strict-dynamic'",
"require-trusted-types-for": "'script'",
"trusted-types": "html url",
}
if user != nil {
if user.ExternalFontHosts != "" {
policies["font-src"] = user.ExternalFontHosts
if user.Stylesheet != "" {
policies["style-src"] += " " + user.ExternalFontHosts
}
}
}
var policy strings.Builder
// This is needed to always have the same order.
for _, key := range order {
if value, ok := policies[key]; ok {
policy.WriteString(key)
policy.WriteString(" ")
policy.WriteString(value)
policy.WriteString("; ")
}
}
return `<meta http-equiv="Content-Security-Policy" content="` + policy.String() + `">`
}
func dict(values ...any) (map[string]any, error) { func dict(values ...any) (map[string]any, error) {
if len(values)%2 != 0 { if len(values)%2 != 0 {
return nil, fmt.Errorf("dict expects an even number of arguments") return nil, fmt.Errorf("dict expects an even number of arguments")

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"miniflux.app/v2/internal/locale" "miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/model"
) )
func TestDict(t *testing.T) { func TestDict(t *testing.T) {
@ -159,3 +160,11 @@ func TestFormatFileSize(t *testing.T) {
} }
} }
} }
func TestCSP(t *testing.T) {
want := `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src * data:; media-src *; frame-src *; style-src 'nonce-1234'; script-src 'nonce-1234' 'strict-dynamic'; font-src test.com; require-trusted-types-for 'script'; trusted-types html url; ">`
got := csp(&model.User{ExternalFontHosts: "test.com"}, "1234")
if got != want {
t.Errorf(`Unexpected result, got %q instead of %q`, got, want)
}
}

View file

@ -25,24 +25,18 @@
<link rel="apple-touch-icon" sizes="167x167" href="{{ route "appIcon" "filename" "icon-167.png" }}"> <link rel="apple-touch-icon" sizes="167x167" href="{{ route "appIcon" "filename" "icon-167.png" }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ route "appIcon" "filename" "icon-180.png" }}"> <link rel="apple-touch-icon" sizes="180x180" href="{{ route "appIcon" "filename" "icon-180.png" }}">
<link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
{{ if .user }}
{{ $cspNonce := nonce }} {{ $cspNonce := nonce }}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; {{ if .user.ExternalFontHosts }}font-src {{ .user.ExternalFontHosts }}; {{ end }}style-src 'self'{{ if .user.Stylesheet }}{{ if .user.ExternalFontHosts }} {{ .user.ExternalFontHosts }}{{ end }} 'nonce-{{ $cspNonce }}'{{ end }}{{ if .user.CustomJS }}; script-src 'self' 'nonce-{{ $cspNonce }}'{{ end }}; require-trusted-types-for 'script'; trusted-types html url;"> {{ csp .user $cspNonce | safeHTML }}
<link rel="stylesheet" nonce="{{ $cspNonce }}" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
<script nonce="{{ $cspNonce }}" src="{{ route "javascript" "name" "app" "checksum" .app_js_checksum }}" type="module"></script>
{{ if .user -}}
{{ if .user.Stylesheet -}} {{ if .user.Stylesheet -}}
<style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style> <style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
{{ end -}} {{ end -}}
{{ if .user.CustomJS -}} {{ if .user.CustomJS -}}
<script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script> <script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script>
{{ end -}} {{ end -}}
{{ else -}}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types html url;">
{{ end -}} {{ end -}}
<script src="{{ route "javascript" "name" "app" "checksum" .app_js_checksum }}" type="module"></script>
</head> </head>
<body <body
data-service-worker-url="{{ route "javascript" "name" "service-worker" "checksum" .sw_js_checksum }}" data-service-worker-url="{{ route "javascript" "name" "service-worker" "checksum" .sw_js_checksum }}"