CSP (Safari-safe): merge -elem/-attr into base; respect explicit disables; no mirror-back; header only for documents/workers

- Add CSP3 support for style/script: include -elem and -attr directives
- Base (style-src, script-src) now unions elem/attr (CSP2/Safari fallback)
- Respect explicit base disables (e.g. style-src.unsafe-inline: false)
- Hashes only when 'unsafe-inline' absent in the final base tokens
- Nginx: set CSP only for HTML/worker via header_filter_by_lua_block; drop for subresources
- Remove per-location header_filter; keep body_filter only
- Update app role flags to *-attr where appropriate; extend desktop CSS sources
- Add comprehensive unit tests for union/explicit-disable/no-mirror-back

Ref: https://chatgpt.com/share/68f87a0a-cebc-800f-bb3e-8c8ab4dee8ee
This commit is contained in:
2025-10-22 13:53:06 +02:00
parent 1eefdea050
commit 57d5269b07
43 changed files with 366 additions and 119 deletions

View File

@@ -10,17 +10,6 @@
lua_need_request_body on;
header_filter_by_lua_block {
local ct = ngx.header.content_type or ""
if ct:lower():find("^text/html") then
ngx.ctx.is_html = true
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
ngx.header.content_length = nil
else
ngx.ctx.is_html = false
end
}
body_filter_by_lua_block {
-- Only process HTML responses
if not ngx.ctx.is_html then

View File

@@ -1,2 +1,33 @@
add_header Content-Security-Policy "{{ applications | build_csp_header(application_id, domains) }}" always;
proxy_hide_header Content-Security-Policy; # Todo: Make this optional
# ===== Content Security Policy: only for documents and workers (no locations needed) =====
# 1) Define your CSP once (Jinja: escape double quotes to be safe)
set $csp "{{ applications | build_csp_header(application_id, domains) | replace('\"','\\\"') }}";
# 2) Send CSP ONLY for document responses; also for workers via Sec-Fetch-Dest
header_filter_by_lua_block {
local ct = ngx.header.content_type or ngx.header["Content-Type"] or ""
local dest = ngx.var.http_sec_fetch_dest or ""
local lct = ct:lower()
local is_html = lct:find("^text/html") or lct:find("^application/xhtml+xml")
local is_worker = (dest == "worker") or (dest == "serviceworker")
if is_html or is_worker then
ngx.header["Content-Security-Policy"] = ngx.var.csp
else
ngx.header["Content-Security-Policy"] = nil
ngx.header["Content-Security-Policy-Report-Only"] = nil
end
-- If you'll modify the body later, drop Content-Length on HTML
if is_html then
ngx.ctx.is_html = true
ngx.header.content_length = nil
else
ngx.ctx.is_html = false
end
}
# 3) Prevent upstream/app CSP (duplicates)
proxy_hide_header Content-Security-Policy;
proxy_hide_header Content-Security-Policy-Report-Only;

View File

@@ -18,10 +18,10 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -37,5 +37,5 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true

View File

@@ -13,7 +13,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -27,7 +27,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -29,7 +29,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -15,6 +15,8 @@ server:
- https://code.jquery.com/
style-src-elem:
- https://cdn.jsdelivr.net
- https://kit.fontawesome.com
- https://code.jquery.com/
font-src:
- https://ka-f.fontawesome.com
- https://cdn.jsdelivr.net
@@ -25,7 +27,7 @@ server:
frame-src:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
script-src:
script-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -10,7 +10,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -12,9 +12,7 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
connect-src:

View File

@@ -18,10 +18,10 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
oauth2_proxy:
application: "application"

View File

@@ -27,7 +27,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -24,7 +24,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -29,7 +29,7 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
domains:

View File

@@ -14,7 +14,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -19,9 +19,9 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-src:

View File

@@ -18,12 +18,12 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-inline: true
domains:
aliases: []

View File

@@ -16,11 +16,11 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
rbac:

View File

@@ -17,12 +17,12 @@ server:
style-src-elem:
- https://fonts.googleapis.com
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
unsafe-eval: true
domains:

View File

@@ -27,12 +27,12 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
connect-src:

View File

@@ -29,7 +29,7 @@ server:
frame-ancestors:
- "*" # No damage if it's used somewhere on other websites, it anyhow looks like art
flags:
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -26,11 +26,11 @@ server:
font-src: []
frame-ancestors: []
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
domains:
canonical:

View File

@@ -10,7 +10,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
domains:
canonical:

View File

@@ -12,9 +12,9 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
script-src:
script-src-attr:
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
unsafe-eval: true
whitelist:

View File

@@ -19,9 +19,9 @@ server:
# Makes sense that all of the website content is available in the navigator
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true

View File

@@ -2,11 +2,11 @@ version: "production" # @see https://nextcloud.com/blog/nex
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
font-src:

View File

@@ -23,7 +23,7 @@ server:
flags:
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -17,11 +17,6 @@ server:
flags:
script-src-elem:
unsafe-inline: true
#script-src:
# unsafe-inline: true
# unsafe-eval: true
#style-src:
# unsafe-inline: true
whitelist:
font-src: []
connect-src: []

View File

@@ -10,9 +10,9 @@ server:
flags:
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-ancestors:

View File

@@ -16,7 +16,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -15,7 +15,7 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true

View File

@@ -9,13 +9,13 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
unsafe-inline: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
frame-ancestors:

View File

@@ -13,12 +13,12 @@ server:
aliases: []
csp:
flags:
script-src:
script-src-attr:
unsafe-inline: true
unsafe-eval: true
script-src-elem:
unsafe-inline: true
style-src:
style-src-attr:
unsafe-inline: true
whitelist:
font-src:

View File

@@ -6,12 +6,12 @@ features:
server:
csp:
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
domains:
canonical:

View File

@@ -69,9 +69,9 @@ server:
script-src-elem:
unsafe-inline: true
unsafe-eval: true
style-src:
style-src-attr:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
domains:
canonical:

View File

@@ -17,11 +17,11 @@ features:
server:
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-eval: true
whitelist:
worker-src:

View File

@@ -32,7 +32,7 @@ server:
worker-src:
- "blob:"
flags:
script-src:
script-src-attr:
unsafe-eval: true
script-src-elem:
unsafe-inline: true

View File

@@ -20,11 +20,11 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true
script-src:
script-src-attr:
unsafe-inline: true
locations:
admin: "/admin/"

View File

@@ -8,7 +8,7 @@ server:
frame-ancestors:
- "{{ WEB_PROTOCOL }}://*.{{ PRIMARY_DOMAIN }}"
flags:
style-src:
style-src-attr:
unsafe-inline: true
docker:
services:

View File

@@ -11,7 +11,7 @@ server:
aliases: []
csp:
flags:
style-src:
style-src-attr:
unsafe-inline: true
script-src-elem:
unsafe-inline: true