From e28d4fad8c2df34ff6720c0f83829b5d86d96991 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 15 May 2026 20:23:29 +0200 Subject: [PATCH] l4d2web/csp: allow Steam avatar CDN in img-src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-state grid renders player avatars as , but the CSP img-src directive was `'self' data:` — so the browser silently blocked every avatar load, leaving placeholder circles in place. The DB cache and Steam API path were both healthy; only the browser-side load was blocked. Use the wildcard *.steamstatic.com host-source rather than pinning a single hostname: Steam rotates avatars across steamcdn-a.akamaihd.net, avatars.akamai/cloudflare/fastly.steamstatic.com over time, and a single-hostname allowlist would re-break on the next shuffle. Test now pins img-src explicitly — the previous assertions only checked default-src/frame-ancestors/form-action, so a regression of this exact line would have silently passed. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/app.py | 4 +++- l4d2web/tests/test_security.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/l4d2web/app.py b/l4d2web/app.py index 1e28f5b..c1215cc 100644 --- a/l4d2web/app.py +++ b/l4d2web/app.py @@ -90,12 +90,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). response.headers.setdefault( "Content-Security-Policy", "default-src 'self'; " f"script-src 'self' 'nonce-{nonce}'; " "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data:; " + "img-src 'self' data: https://*.steamstatic.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 7e1cdc4..b3db104 100644 --- a/l4d2web/tests/test_security.py +++ b/l4d2web/tests/test_security.py @@ -79,6 +79,10 @@ def test_security_headers_present(client) -> None: assert "default-src 'self'" in csp assert "frame-ancestors 'none'" in csp 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. + assert "img-src 'self' data: https://*.steamstatic.com" in csp def test_csp_nonce_matches_inline_script(client) -> None: