- 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
36 lines
1.2 KiB
Python
36 lines
1.2 KiB
Python
"""Environment Jinja2 partagé (A10 : autoescape activé partout).
|
|
|
|
Une seule instance au lieu de 29 `Environment(loader=FileSystemLoader(...))`
|
|
sans autoescape — 326 interpolations `{{ … }}` étaient servies crues et tous
|
|
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"]),
|
|
)
|
|
|
|
|
|
# A40 : version des assets statiques première main, lue une seule fois (le
|
|
# fichier VERSION fait foi ; repli "dev" si l'image ne le contient pas).
|
|
try:
|
|
ASSET_VERSION = (pathlib.Path(__file__).resolve().parent.parent / "VERSION").read_text(
|
|
encoding="utf-8"
|
|
).strip()
|
|
except OSError: # pragma: no cover
|
|
ASSET_VERSION = "dev"
|
|
|
|
ENV.globals["asset_version"] = ASSET_VERSION
|
|
ENV.globals["csp_nonce"] = lambda: CSP_NONCE.get()
|