Files
ObsiGate/tests/test_security_headers.py
T
Spark 287afa1537
CI / lint (push) Successful in 3m5s
CI / security (push) Failing after 1m49s
CI / test (push) Successful in 5m19s
CI / build (push) Successful in 1m36s
CI / e2e (push) Successful in 18m9s
fix: CSP — frame-src autorise la console du service d'agents (mode console du panneau Spark) BUG-119
2026-10-24 03:10:34 +00:00

102 lines
4.0 KiB
Python

"""Tests — cookies Secure, CORS same-origin explicite, avertissement bind (ROADMAP #87 T3/T8).
- `is_secure_cookies()` : `OBSIGATE_SECURE_COOKIES=true|false|auto` (défaut
`auto` : Secure si la requête arrive en https, sinon pas de flag — les
navigateurs ignorent les cookies `Secure` en clair).
- CORS same-origin EXPLICITE : `CORSMiddleware(allow_origins=[])` — aucun
`Access-Control-Allow-*` n'est émis même avec un `Origin` cross-origin,
et les preflights sont rejetés (400).
"""
from __future__ import annotations
def test_secure_cookies_default_auto(monkeypatch):
"""Défaut `auto` : sans requête → pas de flag (logins HTTP locaux préservés)."""
from backend.auth.router import is_secure_cookies
monkeypatch.delenv("OBSIGATE_SECURE_COOKIES", raising=False)
assert is_secure_cookies() is False
def test_secure_cookies_forced_values(monkeypatch):
"""`true`/`1`/`yes` → flag ; `false`/`0`/`no` → pas de flag (insensible à la casse)."""
from backend.auth.router import is_secure_cookies
for value in ("true", "True", "TRUE", "1", "yes", "on"):
monkeypatch.setenv("OBSIGATE_SECURE_COOKIES", value)
assert is_secure_cookies() is True
for value in ("false", "False", "FALSE", "0", "no", "off"):
monkeypatch.setenv("OBSIGATE_SECURE_COOKIES", value)
assert is_secure_cookies() is False
def test_no_cors_headers_on_api(client):
"""Pas de `Access-Control-Allow-Origin` : same-origin imposé par le navigateur."""
resp = client.get("/api/health")
assert resp.status_code == 200
assert "access-control-allow-origin" not in {k.lower() for k in resp.headers}
def test_no_cors_headers_on_public_share(client):
"""Idem sur la page publique de partage."""
resp = client.get("/s/jeton-inexistant")
assert resp.status_code == 404
assert "access-control-allow-origin" not in {k.lower() for k in resp.headers}
def test_cross_origin_get_emits_no_acao(client):
"""#87 T8 : même avec un `Origin` cross-origin, aucun ACAO (refus explicite)."""
resp = client.get("/api/health", headers={"Origin": "http://evil.example"})
assert resp.status_code == 200
assert "access-control-allow-origin" not in {k.lower() for k in resp.headers}
def test_cross_origin_preflight_rejected(client):
"""#87 T8 : preflight cross-origin → 400 (origine non autorisée)."""
resp = client.options(
"/api/health",
headers={
"Origin": "http://evil.example",
"Access-Control-Request-Method": "GET",
},
)
assert resp.status_code == 400
def test_security_headers_present(client):
"""En-têtes de durcissement posés par le middleware (non-régression)."""
resp = client.get("/api/health")
assert resp.headers.get("x-content-type-options") == "nosniff"
assert resp.headers.get("x-frame-options") == "SAMEORIGIN"
csp = resp.headers.get("content-security-policy", "")
assert "object-src 'none'" in csp
assert "frame-ancestors 'self'" in csp
def _csp_directives(csp: str) -> dict[str, list[str]]:
"""Découpe une politique CSP en ``{directive: [sources]}``."""
directives: dict[str, list[str]] = {}
for part in csp.split(";"):
tokens = part.split()
if tokens:
directives[tokens[0]] = tokens[1:]
return directives
def test_csp_frame_src_allows_agent_console(client):
"""BUG-119 : le « mode console » du panneau Spark charge la console du
service d'agents (``https://agent.dracodev.net``) dans une iframe —
``frame-src`` doit autoriser cette origine, sinon le navigateur bloque
le chargement (« This content is blocked »). Les sources historiques
(``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune
autre origine n'est admise."""
resp = client.get("/api/health")
csp = resp.headers.get("content-security-policy", "")
directives = _csp_directives(csp)
assert directives.get("frame-src") == [
"'self'",
"blob:",
"https://agent.dracodev.net",
]