Files
ObsiGate/tests/test_csp_nonce.py
T

83 lines
2.9 KiB
Python

"""Tests — nonces CSP (ROADMAP #87 T5b).
- `inject_csp_nonce` ne touche que les scripts inline exécutables
(`<script>`, `type="module"` / `type="importmap"` sans `src`), jamais les
blocs de données (`type="text/plain"`) ni les scripts externes.
- Chaque page HTML servie avec des scripts inline les porte tous avec un
nonce après injection.
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
NONCE = "TESTNONCE1234567890"
def _read(name: str) -> str:
return (ROOT / "frontend" / name).read_text(encoding="utf-8")
def test_inject_only_bare_executable_scripts():
from backend.csp import inject_csp_nonce
html = (
"<script>var a = 1;</script>"
'<script type="module">import x from "y";</script>'
'<script type="importmap">{"imports": {}}</script>'
'<script type="module" src="/static/js/app.js"></script>'
'<script src="https://cdnjs.cloudflare.com/x.js"></script>'
'<script id="raw-content" type="text/plain">hello</script>'
'<script nonce="OLD">var b = 2;</script>'
)
out = inject_csp_nonce(html, NONCE)
assert out.count(f'nonce="{NONCE}"') == 3
assert '<script src="https://cdnjs.cloudflare.com/x.js">' in out
assert '<script id="raw-content" type="text/plain">' in out
assert '<script nonce="OLD">' in out
def test_new_nonce_unique_per_call():
from backend.csp import new_nonce
assert new_nonce() != new_nonce()
def test_all_pages_fully_nonced():
"""Aucun script inline exécutable sans nonce après injection (sauf src=)."""
from backend.csp import inject_csp_nonce
for name in ("index.html", "popout.html", "admin.html", "editor-poc.html", "excalidraw-editor.html"):
out = inject_csp_nonce(_read(name), NONCE)
bare = re.findall(r"<script>", out)
assert not bare, f"{name} : scripts sans nonce restants"
inline_mods = [m for m in re.findall(r'<script type="(?:module|importmap)">', out)]
assert not inline_mods, f"{name} : modules/importmap sans nonce restants"
def _nonce_of(csp: str) -> str | None:
m = re.search(r"'nonce-([^']+)'", csp or "")
return m.group(1) if m else None
def test_nonce_header_fresh_per_response(client):
"""Chaque réponse porte un nonce frais dans `script-src`."""
r1 = client.get("/")
r2 = client.get("/")
n1, n2 = _nonce_of(r1.headers.get("content-security-policy")), _nonce_of(
r2.headers.get("content-security-policy")
)
assert n1 and n2 and n1 != n2
def test_nonce_matches_injected_html(client):
"""Le nonce de l'en-tête est celui injecté dans le HTML (`/`, excalidraw)."""
for path in ("/", "/excalidraw-editor.html"):
resp = client.get(path)
assert resp.status_code == 200, path
nonce = _nonce_of(resp.headers.get("content-security-policy"))
assert nonce, path
assert f'nonce="{nonce}"' in resp.text, path