Merge pull request 'fix(security): add unsafe-eval to CSP script-src (Alpine.js runtime requires it)' (#14) from fm/fix-denya-csp-unsafe-eval-20260909 into main
This commit was merged in pull request #14.
This commit is contained in:
@@ -98,9 +98,15 @@ Frontend: Alpine.js + Tailwind CSS vendored same-origin (no CDN) — see
|
||||
`alpine-3.17.2.min.js`; the tailwind play file does not, e.g.
|
||||
`tailwind-3.4.17.js`), then bump the `<script src>` + the
|
||||
`VENDORED_SCRIPTS` tuple in the regression file `tests/test_frontend_vendoring.py`.
|
||||
- HTML pages ship `Cache-Control: no-cache` and CSP is self-only
|
||||
(`script-src`/`style-src 'self' 'unsafe-inline'`, `connect-src 'self'`); no
|
||||
CDN host is allowed in CSP (`app/main.py::SecurityHeadersMiddleware`).
|
||||
- HTML pages ship `Cache-Control: no-cache` and CSP allows no external host
|
||||
(`script-src 'self' 'unsafe-inline' 'unsafe-eval'`, `style-src 'self'
|
||||
'unsafe-inline'`, `connect-src 'self'`); no CDN host is allowed in CSP
|
||||
(`app/main.py::SecurityHeadersMiddleware`). `'unsafe-eval'` is required by
|
||||
the Alpine 3.17.2 CDN build: its evaluator compiles every `x-*` expression
|
||||
with `new Function()`, and without it CSP blocks Alpine entirely (stuck
|
||||
loading overlay on every page) — covered by
|
||||
`test_csp_script_src_allows_unsafe_eval_for_alpine` in
|
||||
`tests/test_frontend_vendoring.py`.
|
||||
|
||||
### Tickets (Sprint 2)
|
||||
| Method | Path | Auth | Description |
|
||||
@@ -139,7 +145,8 @@ Frontend: Alpine.js + Tailwind CSS vendored same-origin (no CDN) — see
|
||||
- Use `require_roles(*ADMIN_ROLES)` for admin gates; `sub` claim holds string user ID
|
||||
- Security headers middleware in `app/main.py`: X-Frame-Options DENY +
|
||||
nosniff on everything, CSP on HTML pages, HSTS when `X-Forwarded-Proto: https`
|
||||
(CSP is self-only — frontend libs are vendored, see "Frontend assets")
|
||||
(CSP allows no external host — frontend libs are vendored, see "Frontend
|
||||
assets"; `script-src` carries `'unsafe-eval'` for the Alpine runtime)
|
||||
|
||||
## Ticket System (Sprint 2)
|
||||
|
||||
|
||||
+7
-3
@@ -144,8 +144,12 @@ class SecurityHeadersMiddleware:
|
||||
* CSP on HTML pages (login + app pages). Alpine.js and Tailwind are
|
||||
vendored same-origin (``/static/vendor/``), so no external hosts are
|
||||
allowed and the page is fully self-contained — safe on LAN-only demo
|
||||
clients. Inline scripts/styles stay enabled for the Alpine/tailwind
|
||||
runtime;
|
||||
clients. ``script-src`` keeps ``'unsafe-inline'`` for the inline
|
||||
``tailwind.config``/``app()`` blocks and ``'unsafe-eval'`` because
|
||||
Alpine 3.17.2's expression evaluator compiles ``x-data``/``x-show``/
|
||||
``x-text`` etc. with ``new Function()`` — without ``'unsafe-eval'`` CSP
|
||||
blocks every Alpine expression and the loading overlay never clears;
|
||||
style-src keeps ``'unsafe-inline'`` for the Tailwind runtime;
|
||||
* ``Cache-Control: no-cache`` on HTML pages so templates always
|
||||
revalidate (the vendored assets themselves are cached immutably via
|
||||
versioned filenames);
|
||||
@@ -156,7 +160,7 @@ class SecurityHeadersMiddleware:
|
||||
HSTS = "max-age=31536000; includeSubDomains"
|
||||
CSP = (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline'; "
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"font-src 'self' data:; "
|
||||
|
||||
@@ -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