From 2400cc2ea109edef45be4c23b72865742a34983a Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 11 Sep 2026 23:22:11 +0200 Subject: [PATCH] fix(csp): ship the interface strings as a JSON data block, not an inline script base.html.j2 set window.I18N from an executable inline " cannot end the block early. Integration tests require that a page ships no executable inline script and that a catalogue string containing "" survives the round trip through the block. Co-Authored-By: Claude Opus 5 (1M context) --- app/static/js/modal.js | 3 +++ app/templates/moduls/base.html.j2 | 2 +- tests/integration/test_app_routes.py | 32 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/static/js/modal.js b/app/static/js/modal.js index 470071a..013d143 100644 --- a/app/static/js/modal.js +++ b/app/static/js/modal.js @@ -1,3 +1,6 @@ +const i18nBlock = document.getElementById('i18n'); +window.I18N = i18nBlock ? JSON.parse(i18nBlock.textContent) : {}; + function t(source) { return (window.I18N || {})[source] || source; } diff --git a/app/templates/moduls/base.html.j2 b/app/templates/moduls/base.html.j2 index 4955545..2712fa1 100644 --- a/app/templates/moduls/base.html.j2 +++ b/app/templates/moduls/base.html.j2 @@ -71,7 +71,7 @@ {% include "moduls/modal.html.j2" %} - + {% for name in [ 'modal', 'navigation', diff --git a/tests/integration/test_app_routes.py b/tests/integration/test_app_routes.py index c2bcb0f..18e0a73 100644 --- a/tests/integration/test_app_routes.py +++ b/tests/integration/test_app_routes.py @@ -1,5 +1,6 @@ import json import os +import re import shutil import subprocess import sys @@ -111,6 +112,37 @@ class TestEscaping(AppRouteMixin, unittest.TestCase): self.assertIn("<script>alert('config')", body) +class TestContentSecurityPolicy(AppRouteMixin, unittest.TestCase): + def test_page_ships_no_executable_inline_script(self): + body = self.client.get("/de/").get_data(as_text=True) + + inline = [ + tag + for tag in re.findall(r"]*>", body) + if "src=" not in tag and 'type="application/json"' not in tag + ] + + self.assertEqual( + inline, + [], + "a host CSP can only hash an inline script whose content it knows, " + "and this one changes with every language", + ) + + def test_interface_strings_ship_as_a_json_data_block(self): + i18n._catalogs["de"] = {"Open": ""} + + body = self.client.get("/de/").get_data(as_text=True) + block = re.search( + r'', body, re.S + ) + + self.assertIsNotNone(block) + self.assertEqual( + json.loads(block.group(1))["Open"], "" + ) + + class TestApodBackground(AppRouteMixin, unittest.TestCase): def setUp(self): super().setUp()