mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-11 21:46:47 +00:00
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 <script>. A host CSP can only allow an inline script by hash or by 'unsafe-inline', and this script's content changes with every language, so no hash can cover it. Infinito.Nexus serves the dashboard with a hash-based script-src-elem whenever its logout feature is off, and there the script was blocked: every page logged "Executing inline script violates the following Content Security Policy directive 'script-src-elem ...'" and window.I18N stayed undefined. The strings now ship as <script id="i18n" type="application/json">, which the browser does not execute and CSP does not govern; modal.js parses the block before its first use. tojson escapes <, > and &, so a string that contains "</script>" cannot end the block early. Integration tests require that a page ships no executable inline script and that a catalogue string containing "</script>" survives the round trip through the block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
const i18nBlock = document.getElementById('i18n');
|
||||||
|
window.I18N = i18nBlock ? JSON.parse(i18nBlock.textContent) : {};
|
||||||
|
|
||||||
function t(source) {
|
function t(source) {
|
||||||
return (window.I18N || {})[source] || source;
|
return (window.I18N || {})[source] || source;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Include modal -->
|
<!-- Include modal -->
|
||||||
{% include "moduls/modal.html.j2" %}
|
{% include "moduls/modal.html.j2" %}
|
||||||
<script>window.I18N = {{ ui_strings | tojson }};</script>
|
<script id="i18n" type="application/json">{{ ui_strings | tojson }}</script>
|
||||||
{% for name in [
|
{% for name in [
|
||||||
'modal',
|
'modal',
|
||||||
'navigation',
|
'navigation',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -111,6 +112,37 @@ class TestEscaping(AppRouteMixin, unittest.TestCase):
|
|||||||
self.assertIn("<script>alert('config')", body)
|
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"<script\b[^>]*>", 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": "</script><script>alert(1)</script>"}
|
||||||
|
|
||||||
|
body = self.client.get("/de/").get_data(as_text=True)
|
||||||
|
block = re.search(
|
||||||
|
r'<script id="i18n" type="application/json">(.*?)</script>', body, re.S
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(block)
|
||||||
|
self.assertEqual(
|
||||||
|
json.loads(block.group(1))["Open"], "</script><script>alert(1)</script>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestApodBackground(AppRouteMixin, unittest.TestCase):
|
class TestApodBackground(AppRouteMixin, unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|||||||
Reference in New Issue
Block a user