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/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'; " 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.