From 6cef55f900be88cf6aabf9e230e83a448f271262 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sat, 16 May 2026 11:22:30 +0200 Subject: [PATCH] fix(csp): allow workshop preview thumbnails from steamusercontent.com Steam serves workshop preview images from images.steamusercontent.com, which the previous img-src whitelist did not cover, so the browser silently blocked every in _overlay_item_table.html. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/l4d2web/app.py | 6 +++--- l4d2web/tests/test_security.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/l4d2web/l4d2web/app.py b/l4d2web/l4d2web/app.py index 86e3113..0fce842 100644 --- a/l4d2web/l4d2web/app.py +++ b/l4d2web/l4d2web/app.py @@ -93,14 +93,14 @@ def create_app(test_config: dict[str, object] | None = None) -> Flask: nonce = getattr(g, "csp_nonce", "") # script-src nonce blocks inline XSS; style 'unsafe-inline' kept for # htmx's auto-injected indicator styles. img/data: for SVG icons. - # *.steamstatic.com covers Steam avatar hosts (avatars.steamstatic.com - # plus the cloudflare/akamai/fastly mirrors they rotate through). + # *.steamstatic.com covers Steam avatar hosts; *.steamusercontent.com + # serves Workshop item preview thumbnails. response.headers.setdefault( "Content-Security-Policy", "default-src 'self'; " f"script-src 'self' 'nonce-{nonce}'; " "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: https://*.steamstatic.com; " + "img-src 'self' data: https://*.steamstatic.com https://*.steamusercontent.com; " "connect-src 'self'; " "frame-ancestors 'none'; " "base-uri 'self'; " diff --git a/l4d2web/tests/test_security.py b/l4d2web/tests/test_security.py index b3db104..d3af5f0 100644 --- a/l4d2web/tests/test_security.py +++ b/l4d2web/tests/test_security.py @@ -81,8 +81,10 @@ def test_security_headers_present(client) -> None: assert "form-action 'self'" in csp # Steam avatar CDN must be explicitly allowed; otherwise the browser # silently blocks the avatar loads and the live-state grid shows - # placeholder circles with names but no faces. + # placeholder circles with names but no faces. Workshop preview thumbnails + # live on a separate host (*.steamusercontent.com) and must also be allowed. assert "img-src 'self' data: https://*.steamstatic.com" in csp + assert "https://*.steamusercontent.com" in csp def test_csp_nonce_matches_inline_script(client) -> None: