Ajoute les en-têtes de sécurité et le drapeau Secure sur le cookie

Le cookie de session était émis sans Secure : en cas de downgrade HTTP ou
de mixed content, il aurait transité en clair. Aucun en-tête de sécurité
n'était positionné (pas de CSP, pas de X-Frame-Options, pas de HSTS), ce
qui laissait le champ libre au clickjacking, aux sniffing MIME et aux
fuites via Referer.

Changements :

1. _set_auth_cookie accepte désormais la request : le drapeau Secure est
   posé automatiquement si scheme == 'https', ou si la nouvelle clé
   config.force_secure_cookies est vraie. En HTTP local (dev/test), le
   cookie reste sans Secure pour ne pas casser les navigateurs.

2. Le middleware CSRF injecte maintenant les en-têtes de sécurité sur
   toutes les réponses :
   - X-Content-Type-Options: nosniff
   - X-Frame-Options: DENY
   - Referrer-Policy: same-origin
   - Content-Security-Policy (default-src 'self', frame-ancestors 'none',
     base-uri 'self', avec autorisation pour Google Fonts et unsafe-inline
     pour les JS/CSS inline des templates semaine/stats ; à durcir plus tard)
   - Strict-Transport-Security (HTTPS uniquement)

Les nouveaux tests vérifient la présence des headers, le HSTS en HTTPS
uniquement, le Secure en HTTPS, l'absence de Secure en HTTP, et l'override
par force_secure_cookies.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-20 00:35:15 +02:00
parent c1a2f38044
commit ca99ca85a2
2 changed files with 115 additions and 5 deletions

View File

@ -421,3 +421,80 @@ def test_presence_endpoints_require_post():
assert r_post_arr.status_code == 200
assert r_post_dep.status_code == 200
assert r_post_arr.json() == {"ok": True}
# ── En-têtes de sécurité + cookie Secure ──────────────────────────────────────
def test_security_headers_present_on_every_response():
"""Les en-têtes X-Content-Type-Options, X-Frame-Options, CSP et Referrer-Policy
doivent être positionnés sur toutes les réponses."""
r = client.get("/login")
assert r.headers.get("X-Content-Type-Options") == "nosniff"
assert r.headers.get("X-Frame-Options") == "DENY"
assert r.headers.get("Referrer-Policy") == "same-origin"
csp = r.headers.get("Content-Security-Policy", "")
assert "default-src 'self'" in csp
assert "frame-ancestors 'none'" in csp
assert "base-uri 'self'" in csp
def test_hsts_header_only_on_https():
"""HSTS ne doit être positionné qu'en HTTPS."""
# En HTTP (TestClient par défaut), pas de HSTS
r = client.get("/login")
assert "Strict-Transport-Security" not in r.headers
# En HTTPS, HSTS doit être présent
https_client = TestClient(main.app, base_url="https://testserver")
r2 = https_client.get("/login")
assert r2.headers.get("Strict-Transport-Security", "").startswith("max-age=31536000")
def test_cookie_has_secure_flag_over_https():
"""Sur une requête HTTPS, le cookie de session doit porter le drapeau Secure."""
https_client = TestClient(main.app, base_url="https://testserver")
_seed_user("kate")
r = https_client.post(
"/login/password",
data={"email": "kate@x.fr", "password": "x" * 12},
follow_redirects=False,
)
assert r.status_code == 302
cookie = r.headers.get("set-cookie", "")
assert "Secure" in cookie, cookie
assert "HttpOnly" in cookie
assert "SameSite=lax" in cookie
def test_cookie_omits_secure_flag_over_http():
"""En HTTP (dev local), le cookie ne doit pas avoir Secure (sinon il ne serait pas posé)."""
_seed_user("liam")
r = client.post(
"/login/password",
data={"email": "liam@x.fr", "password": "x" * 12},
follow_redirects=False,
)
assert r.status_code == 302
cookie = r.headers.get("set-cookie", "")
assert "HttpOnly" in cookie
assert "SameSite=lax" in cookie
# En HTTP, pas de Secure (sinon le navigateur refuserait le cookie)
assert "Secure" not in cookie
def test_force_secure_cookies_config_overrides_scheme():
"""config.force_secure_cookies doit forcer Secure même en HTTP."""
cfg = models.load_config()
cfg["force_secure_cookies"] = True
models.save_config(cfg)
try:
_seed_user("mona")
r = client.post(
"/login/password",
data={"email": "mona@x.fr", "password": "x" * 12},
follow_redirects=False,
)
assert "Secure" in r.headers.get("set-cookie", "")
finally:
cfg["force_secure_cookies"] = False
models.save_config(cfg)