securite: #87 T5b nonces CSP prets pour bascule (sans changement)
This commit is contained in:
@@ -100,7 +100,7 @@ await test("module exports renderExcalidraw + helpers", () => {
|
||||
assert.equal(typeof destroyExcalidrawEditor, "function");
|
||||
});
|
||||
|
||||
await test("renderExcalidraw creates an iframe with sandbox + static src", () => {
|
||||
await test("renderExcalidraw creates an iframe with sandbox + routed src", () => {
|
||||
const container = document.getElementById("content-area");
|
||||
const data = {
|
||||
is_excalidraw: true,
|
||||
@@ -111,7 +111,8 @@ await test("renderExcalidraw creates an iframe with sandbox + static src", () =>
|
||||
renderExcalidraw(container, data, "TestVault", "diagram.excalidraw");
|
||||
const iframe = container.querySelector("iframe");
|
||||
assert.ok(iframe, "iframe should be created");
|
||||
assert.ok(iframe.src.includes("/static/excalidraw-editor.html"), `src: ${iframe.src}`);
|
||||
assert.ok(iframe.src.includes("/excalidraw-editor.html"), `src: ${iframe.src}`);
|
||||
assert.ok(!iframe.src.includes("/static/excalidraw-editor.html"), `route avec nonce CSP: ${iframe.src}`);
|
||||
assert.ok(iframe.sandbox.contains("allow-scripts"), "sandbox allow-scripts");
|
||||
assert.ok(iframe.sandbox.contains("allow-same-origin"), "sandbox allow-same-origin");
|
||||
assert.match(iframe.style.cssText, /100%/);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user