From 451b2406d0382efaf33aa9b43f5c320aea9646a4 Mon Sep 17 00:00:00 2001 From: Carolina <79524656+carochacs@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:02:09 -0600 Subject: [PATCH 1/2] fix(security): add worker-src to CSP so alphaTab-based plugins can render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The baseline CSP headers added in 81837ec (#44, XSS fix) set script-src, style-src, img-src, font-src, media-src, and connect-src, but no worker-src. Per CSP fallback rules, workers then fall back to script-src, which allows 'self'/'unsafe-inline'/https: but not blob:. alphaTab (used by the tabview and staffview plugins) spawns its rendering worker from a blob: URL. Without worker-src, browsers silently blocked that worker, so both plugins rendered a blank canvas for every song and every arrangement — not specific to any one instrument. Reported as "Sax arrangement not visible in staffview or tabview" for the song Money; reproduced the same blank result on the Lead arrangement, confirming it was a general regression from the CSP change rather than anything arrangement-specific. Adds "worker-src 'self' blob:;", matching the existing blob: allowance already granted to img-src/media-src for the same reason (plugin CDN assets). Co-Authored-By: Claude Sonnet 5 --- server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server.py b/server.py index a3ded5ce..7c996bdd 100644 --- a/server.py +++ b/server.py @@ -126,6 +126,7 @@ "img-src 'self' data: blob: https:; " "font-src 'self' data: https:; " "media-src 'self' blob: https:; " + "worker-src 'self' blob:; " "connect-src 'self' https: wss: ws:; " "object-src 'none'; " "base-uri 'self'; " From 0475afdd27b3cdb428031c02daccbbd350e03cae Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 21:44:09 +0000 Subject: [PATCH 2/2] Address pullfrog review comments on the worker-src CSP fix - Add a regression test pinning the new worker-src directive ('self' and blob: present, no bare http:/data:), mirroring the existing script-src parsing test so a future CSP edit can't silently drop it. - Add the CHANGELOG.md [Unreleased] > Security entry the PR's own checklist asked for, alongside the #47 baseline-CSP entries this builds on. --- CHANGELOG.md | 11 +++++++++++ tests/test_security_headers.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cfa7167..599570bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 externalized today, so a stricter policy would need that rewritten first. Defense-in-depth: this would have limited the blast radius of the retune XSS above (and any future/residual one) even before that fix landed. +- **CSP now allows `blob:` workers, fixing a blank Tab View / Staff View + render.** The baseline CSP added above (#47) covered + `script-src`/`style-src`/`img-src`/`font-src`/`media-src`/`connect-src` + but never set `worker-src`. Per CSP fallback rules, a worker without an + explicit `worker-src` falls back to `script-src`'s policy, which allows + `'self'`/`'unsafe-inline'`/`https:` but not `blob:` — and alphaTab (used + by the `tabview` and `staffview` plugins) spawns its rendering worker + from a `blob:` URL. Without `worker-src`, browsers silently blocked that + worker, rendering a blank canvas for every song. Added + `worker-src 'self' blob:;`, matching the `blob:` allowance already + granted to `img-src`/`media-src` for the same plugin-CDN-asset reason. ### Fixed diff --git a/tests/test_security_headers.py b/tests/test_security_headers.py index e75f7016..f517c94d 100644 --- a/tests/test_security_headers.py +++ b/tests/test_security_headers.py @@ -61,6 +61,21 @@ def test_csp_blocks_non_https_script_origins_but_allows_self_and_https(client): assert "*" not in script_src +def test_csp_allows_self_and_blob_workers(client): + # alphaTab (tabview/staffview) spawns its rendering worker from a blob: + # URL. Without an explicit worker-src, CSP falls back to script-src, + # which allows 'self'/https: but not blob: -- silently blocking the + # worker and rendering a blank canvas (issue: worker-src missing after + # the #44/#47 baseline-CSP addition). + r = client.get("/api/version") + csp = r.headers["Content-Security-Policy"] + worker_src = next(part for part in csp.split(";") if part.strip().startswith("worker-src")) + assert "'self'" in worker_src + assert "blob:" in worker_src + assert "http:" not in worker_src + assert "data:" not in worker_src + + def test_security_headers_present_on_error_responses_too(client): # A 404 (unknown route) must still carry the headers — they're set via # middleware wrapping the whole call_next chain, not per-route.