Files
computer-playbook/roles/sys-front-inj-all/templates/location.lua.j2
Kevin Veen-Birkenbach 57d5269b07 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
2025-10-22 13:53:06 +02:00

81 lines
2.7 KiB
Django/Jinja

{# Jinja macro: expands feature snippets into Lua array pushes at render time #}
{% macro push_snippets(list_name, features) -%}
{% set kind = list_name | regex_replace('_snippets$','') %}
{% for f in features if inj_enabled.get(f) -%}
{{ list_name }}[#{{ list_name }} + 1] = [=[
{%- include 'roles/sys-front-inj-' ~ f ~ '/templates/' ~ kind ~ '_sub.j2' -%}
]=]
{% endfor -%}
{%- endmacro %}
lua_need_request_body on;
body_filter_by_lua_block {
-- Only process HTML responses
if not ngx.ctx.is_html then
return
end
-- Buffer all chunks until EOF
ngx.ctx.buf = ngx.ctx.buf or {}
local chunk, eof = ngx.arg[1], ngx.arg[2]
if chunk ~= "" then
table.insert(ngx.ctx.buf, chunk)
end
if not eof then
-- Swallow intermediate chunks; emit once at EOF
ngx.arg[1] = nil
return
end
-- Concatenate the full HTML
local whole = table.concat(ngx.ctx.buf)
ngx.ctx.buf = nil
-- Remove inline CSP <meta http-equiv="Content-Security-Policy"> (case-insensitive)
local meta_re = [[<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]]
whole = ngx.re.gsub(whole, meta_re, "", "ijo")
-- Build head snippets (rendered by Jinja at template time)
local head_snippets = {}
{{ push_snippets('head_snippets', inj_head_features) }}
local head_payload = table.concat(head_snippets, "\n") .. "</head>"
-- Inject before </head> (first occurrence)
local function repl_head(_) return head_payload end
local new, n, err = ngx.re.sub(whole, [[</head\s*>]], repl_head, "ijo")
if new then
whole = new
else
ngx.log(ngx.WARN, "No </head> found; trying <body> fallback: ", err or "nil")
-- Fallback: inject right AFTER the opening <body ...> tag
local body_open_re = [[<body\b[^>]*>]]
new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. table.concat(head_snippets, "\n"), "ijo")
if new then
whole = new
else
ngx.log(ngx.ERR, "Head-fallback failed: ", err or "nil")
end
end
-- Build body snippets (rendered by Jinja at template time)
local body_snippets = {}
{{ push_snippets('body_snippets', inj_body_features) }}
local body_payload = table.concat(body_snippets, "\n") .. "</body>"
-- Inject before </body> (first occurrence), or append if missing
local function repl_body(_) return body_payload end
new, n, err = ngx.re.sub(whole, [[</body\s*>]], repl_body, "ijo")
if new then
whole = new
else
ngx.log(ngx.WARN, "No </body> found; appending body snippets at end: ", err or "nil")
whole = whole .. table.concat(body_snippets, "\n")
end
-- Emit the modified HTML
ngx.arg[1] = whole or ""
}