fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
  ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
  `script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
  sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
  scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
  helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
  (chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
  : htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
  restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
  : vues chart/map déjà BLOQUÉES par la CSP depuis toujours
  (commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
  JS (A27), resserrer img-src/connect-src

test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)

suite **1037/1037** · `ruff check app tests` OK · docs à jour
This commit is contained in:
2026-10-01 10:41:39 -04:00
parent f706424f90
commit c718fe06de
40 changed files with 175 additions and 52 deletions
+7
View File
@@ -7,9 +7,15 @@ les `|safe` du codebase étaient des no-op.
from __future__ import annotations
import pathlib
from contextvars import ContextVar
from jinja2 import Environment, FileSystemLoader, select_autoescape
# A20 : nonce de script par requête, posé par le middleware CSP, lu par les
# templates via `{{ csp_nonce() }}` (vide hors requête — pas de header CSP
# dans ce cas, donc rien n'est bloqué).
CSP_NONCE: ContextVar[str] = ContextVar("csp_nonce", default="")
ENV = Environment(
loader=FileSystemLoader("app/templates"),
autoescape=select_autoescape(["html"]),
@@ -26,3 +32,4 @@ except OSError: # pragma: no cover
ASSET_VERSION = "dev"
ENV.globals["asset_version"] = ASSET_VERSION
ENV.globals["csp_nonce"] = lambda: CSP_NONCE.get()