fix(csp): add 'unsafe-eval' to script-src so Alpine.js initializes
Alpine 3.17.2's CDN build compiles every x-data/x-show/x-text expression with new Function(), which the strict P0 CSP (script-src 'self' 'unsafe-inline') blocked. Every Alpine directive threw "Evaluating a string as JavaScript violates ... 'unsafe-eval' is not an allowed source", Alpine never initialized, and the loading overlay (x-show="loading" in base.html) stayed visible forever on /login and every Alpine-driven page. Add 'unsafe-eval' to script-src (Alpine's documented CSP requirement for its runtime); everything else in the header is unchanged. Regression test asserts the /login CSP header carries 'unsafe-eval' inside script-src. Verified live: headless chromium (playwright build 1243) shows zero CSP/eval console errors after the fix, with Alpine applying style="display:none" to the loading overlay; the pre-fix header produces the Alpine Expression Error spam and leaves the overlay visible.
This commit is contained in:
@@ -72,3 +72,33 @@ async def test_csp_no_longer_allows_cdn_hosts(client: AsyncClient):
|
||||
assert "script-src 'self' 'unsafe-inline'" in csp
|
||||
assert "style-src 'self' 'unsafe-inline'" in csp
|
||||
assert "connect-src 'self'" in csp
|
||||
|
||||
|
||||
def _directive_sources(csp: str, directive: str) -> list[str]:
|
||||
"""Return the source list of one CSP directive (e.g. ``script-src``)."""
|
||||
for part in csp.split(";"):
|
||||
tokens = part.split()
|
||||
if tokens and tokens[0].strip() == directive:
|
||||
return [t.strip() for t in tokens[1:]]
|
||||
return []
|
||||
|
||||
|
||||
async def test_csp_script_src_allows_unsafe_eval_for_alpine(client: AsyncClient):
|
||||
"""/login CSP must permit 'unsafe-eval' in script-src (Alpine 3.17.2 runtime).
|
||||
|
||||
Alpine's expression evaluator compiles every ``x-data``/``x-show``/``x-text``
|
||||
expression with ``new Function()``. A strict CSP without ``'unsafe-eval'``
|
||||
blocks each evaluation ("Refused to evaluate a string as JavaScript ..."),
|
||||
Alpine never initializes, and the loading overlay (``x-show="loading"`` in
|
||||
base.html) stays visible forever — regression shipped with the P0 CSP.
|
||||
"""
|
||||
resp = await client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
csp = resp.headers["content-security-policy"]
|
||||
script_sources = _directive_sources(csp, "script-src")
|
||||
assert script_sources, f"no script-src directive in CSP: {csp}"
|
||||
assert "'unsafe-eval'" in script_sources, f"script-src missing 'unsafe-eval': {csp}"
|
||||
# Everything else stays as hardened: still 'self'-only apart from the two
|
||||
# Alpine-required relaxations, and connect-src remains 'self'.
|
||||
assert "'self'" in script_sources
|
||||
assert "connect-src 'self'" in csp
|
||||
|
||||
Reference in New Issue
Block a user