fix: vendor math and yaml assets for CSP

This commit is contained in:
ai-ag2026
2026-05-26 19:24:58 +02:00
parent 48a2e79224
commit 815df9de76
71 changed files with 95 additions and 34 deletions

View File

@@ -3,6 +3,11 @@
## [Unreleased]
### Fixed
- Vendored KaTeX CSS, JavaScript, and fonts locally so math rendering no longer triggers CSP font reports for `cdn.jsdelivr.net` font files.
- Vendored `js-yaml` locally so YAML tree-view loading no longer triggers CSP script reports for `cdnjs.cloudflare.com`.
## [v0.51.137] — 2026-05-25 — Release DI (stage-batch19 — 6-PR medium-risk batch)
### Added

View File

@@ -46,7 +46,7 @@ def _security_headers(handler):
"default-src 'self' https://*.cloudflareaccess.com; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
"img-src 'self' data: https: blob:; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' https://cdn.jsdelivr.net; "
"img-src 'self' data: https: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cdn.jsdelivr.net; "
"manifest-src 'self' https://*.cloudflareaccess.com; "
"base-uri 'self'; form-action 'self'"
)

View File

@@ -70,8 +70,8 @@
})()</script>
<link rel="stylesheet" href="static/style.css?v=__WEBUI_VERSION__">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" integrity="sha384-LJcOxlx9IMbNXDqJ2axpfEQKkAYbFjJfhXexLfiRJhjDU81mzgkiQq8rkV0j6dVh" crossorigin="anonymous">
<!-- KaTeX math rendering CSS (loaded eagerly to prevent layout shift) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css" integrity="sha384-5TcZemv2l/9On385z///+d7MSYlvIEw9FuZTIdZ14vJLqWphw7e7ZPuOiCHJcFCP" crossorigin="anonymous">
<!-- KaTeX math rendering CSS (loaded eagerly from vendored assets to prevent font CSP noise) -->
<link rel="stylesheet" href="static/vendor/katex/0.16.22/katex.min.css">
<!-- streaming-markdown: incremental DOM-building markdown parser for live streams -->
<!-- Self-hosted from npm:streaming-markdown@0.2.15 — no CDN dependency. -->
<!-- sha384 of smd.min.js @0.2.15: sha384-T6r95ocN9t3W8tUK2Fa6FPaO7bJryyjyW0WCalrUnpgtm2qXr5xcN4vwPYEJ6vHa -->

View File

@@ -7031,8 +7031,8 @@ function _loadJsyamlThen(cb){
if(_jsyamlLoading){ setTimeout(()=>_loadJsyamlThen(cb),100); return; }
_jsyamlLoading=true;
const s=document.createElement('script');
s.src='https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js';
s.integrity='sha384-8pLvVQkv7pCQqFk7AChLpdEe7gXz9h8GAb7cS0zVeJuKhxR5PU5aEET5pRpHZvxUorzdM';
s.src='static/vendor/js-yaml/4.1.0/js-yaml.min.js';
s.integrity='sha384-+pxiN6T7yvpryuJmE1gM9PX7yQit15auDb+ZwwvJOd/4be2Cie5/IuVXgQb/S9du';
s.crossOrigin='anonymous';
s.onload=()=>{ _jsyamlLoading=false; cb(); };
s.onerror=()=>{ _jsyamlLoading=false; }; // CDN blocked, fall back to raw
@@ -7552,7 +7552,7 @@ function renderKatexBlocks(container){
if(!_katexLoading){
_katexLoading=true;
const script=document.createElement('script');
script.src='https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js';
script.src='static/vendor/katex/0.16.22/katex.min.js';
script.integrity='sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6';
script.crossOrigin='anonymous';
script.onload=()=>{

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -176,14 +176,15 @@ def test_katex_lazy_load_follows_mermaid_pattern():
assert '_katexReady' in UI_JS, '_katexReady flag not found'
def test_katex_js_loaded_from_cdn():
"""KaTeX JS must be loaded from jsdelivr CDN."""
assert 'katex@0.16' in UI_JS, \
'KaTeX JS CDN URL not found in ui.js — expected katex@0.16.x'
def test_katex_js_loaded_from_vendored_asset():
"""KaTeX JS must be loaded from the vendored local asset."""
assert 'static/vendor/katex/0.16.22/katex.min.js' in UI_JS, \
'KaTeX JS vendored URL not found in ui.js — expected local 0.16.22 asset'
assert 'https://cdn.jsdelivr.net/npm/katex@0.16' not in UI_JS
def test_katex_js_has_sri_hash():
"""KaTeX JS CDN tag must have an SRI integrity hash."""
"""KaTeX JS tag must keep an SRI integrity hash for the pinned asset."""
# The hash is in the script.integrity assignment
assert "script.integrity='sha384-" in UI_JS or 'script.integrity="sha384-' in UI_JS, \
'KaTeX JS SRI integrity hash not found in ui.js'
@@ -229,15 +230,16 @@ def test_mermaid_render_failure_removes_temporary_error_dom():
# ── index.html ────────────────────────────────────────────────────────────────
def test_katex_css_in_index_html():
"""KaTeX CSS must be loaded in index.html."""
assert 'katex@0.16' in INDEX, \
'KaTeX CSS CDN link not found in index.html'
"""KaTeX CSS must be loaded from the vendored local asset."""
assert 'static/vendor/katex/0.16.22/katex.min.css' in INDEX, \
'KaTeX CSS vendored link not found in index.html'
assert 'https://cdn.jsdelivr.net/npm/katex@0.16' not in INDEX
def test_katex_css_has_sri_hash():
"""KaTeX CSS link in index.html must have an SRI integrity hash."""
assert 'sha384-5TcZemv2l' in INDEX or 'integrity' in INDEX and 'katex' in INDEX, \
'KaTeX CSS SRI integrity hash not found in index.html'
def test_katex_css_is_pinned_local_asset():
"""KaTeX CSS is pinned by vendored path instead of CDN integrity metadata."""
assert 'static/vendor/katex/0.16.22/katex.min.css' in INDEX, \
'KaTeX CSS local pinned asset not found in index.html'
# ── style.css ─────────────────────────────────────────────────────────────────

View File

@@ -1,26 +1,30 @@
"""Tests for fix #477: KaTeX font-src CSP fix."""
"""Tests for KaTeX font CSP handling.
KaTeX is vendored locally, so the CSP should not need to loosen font-src for a
third-party CDN.
"""
import pathlib
REPO = pathlib.Path(__file__).parent.parent
HELPERS_PY = (REPO / "api" / "helpers.py").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
def test_font_src_allows_jsdelivr():
"""font-src must include cdn.jsdelivr.net for KaTeX fonts."""
assert "font-src 'self' data: https://cdn.jsdelivr.net" in HELPERS_PY, (
"api/helpers.py CSP must allow cdn.jsdelivr.net in font-src "
"so KaTeX math rendering fonts load without console errors."
)
def _font_src() -> str:
return HELPERS_PY.split("font-src", 1)[1].split(";", 1)[0]
def test_font_src_still_allows_self_and_data():
"""font-src must still allow self and data: (used by other font assets)."""
assert "'self'" in HELPERS_PY.split("font-src")[1].split(";")[0]
assert "data:" in HELPERS_PY.split("font-src")[1].split(";")[0]
def test_font_src_keeps_self_and_data_without_cdn_font_exception():
"""font-src should stay tight now that KaTeX fonts are local."""
font_src = _font_src()
assert "'self'" in font_src
assert "data:" in font_src
assert "https://cdn.jsdelivr.net" not in font_src
def test_script_src_already_allows_jsdelivr():
"""script-src already allows cdn.jsdelivr.net — font-src should too."""
assert "https://cdn.jsdelivr.net" in HELPERS_PY.split("font-src")[0], (
"script-src should already allow cdn.jsdelivr.net (KaTeX JS)"
)
def test_katex_assets_are_loaded_from_static_vendor_paths():
assert "static/vendor/katex/0.16.22/katex.min.css" in INDEX_HTML
assert "static/vendor/katex/0.16.22/katex.min.js" in UI_JS
assert "https://cdn.jsdelivr.net/npm/katex@0.16.22" not in INDEX_HTML
assert "https://cdn.jsdelivr.net/npm/katex@0.16.22" not in UI_JS

View File

@@ -0,0 +1,19 @@
"""Regression coverage for vendored js-yaml assets."""
from __future__ import annotations
import pathlib
REPO = pathlib.Path(__file__).parent.parent
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
VENDOR_FILE = REPO / "static" / "vendor" / "js-yaml" / "4.1.0" / "js-yaml.min.js"
def test_runtime_loads_vendored_jsyaml_instead_of_cdnjs():
assert "static/vendor/js-yaml/4.1.0/js-yaml.min.js" in UI_JS
assert "https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js" not in UI_JS
def test_vendored_jsyaml_asset_is_present():
assert VENDOR_FILE.is_file()
content = VENDOR_FILE.read_text(encoding="utf-8")
assert "jsyaml" in content

View File

@@ -0,0 +1,27 @@
"""Regression coverage for vendored KaTeX assets."""
from __future__ import annotations
import pathlib
REPO = pathlib.Path(__file__).parent.parent
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
VENDOR_DIR = REPO / "static" / "vendor" / "katex" / "0.16.22"
def test_index_loads_vendored_katex_css_instead_of_cdn():
assert "static/vendor/katex/0.16.22/katex.min.css" in INDEX_HTML
assert "https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css" not in INDEX_HTML
def test_runtime_loads_vendored_katex_js_instead_of_cdn():
assert "static/vendor/katex/0.16.22/katex.min.js" in UI_JS
assert "https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js" not in UI_JS
def test_vendored_katex_fonts_are_present_for_local_font_src():
css = (VENDOR_DIR / "katex.min.css").read_text(encoding="utf-8")
assert "url(fonts/KaTeX_Main-Regular.woff2)" in css
assert (VENDOR_DIR / "katex.min.js").is_file()
assert (VENDOR_DIR / "fonts" / "KaTeX_Main-Regular.woff2").is_file()
assert (VENDOR_DIR / "fonts" / "KaTeX_AMS-Regular.woff2").is_file()