fix: A19 (partiel) — CSRF réservé aux vrais cas d'exemption (v7.3.5)

- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes
  n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS`
  (`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`,
  `/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`,
  `/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`)
- `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header
  (`adminFetch` prouve que `/api/admin` était déjà couvert)
- reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`),
  `/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch
- tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de
  route du 403 middleware — 4 tests d'anonymat ajustés
- suite **1026/1026** · `ruff check app tests` OK
This commit is contained in:
2026-09-30 23:38:03 -04:00
parent 72fcef2ba9
commit 0861f1fdbf
12 changed files with 61 additions and 16 deletions
+12
View File
@@ -72,6 +72,18 @@ def anon(client):
return client
def anon_csrf(client):
"""Anonyme MAIS CSRF valide — comme un navigateur qui a déjà chargé une page.
Sert aux tests d'"isolation auth" : on veut le 401 de la route, pas le 403
du middleware CSRF qui passerait avant.
"""
anon(client)
client.cookies.set("csrf_token", "csrf-anon")
client.headers["X-CSRF-Token"] = "csrf-anon"
return client
@pytest.fixture
def client():
"""FastAPI TestClient with a fresh temporary SQLite database."""
+3 -1
View File
@@ -1,5 +1,5 @@
"""Non-régression de l'audit sécurité 2026-09-30 — A11 (traversal) et A18 (XSS public)."""
from conftest import anon
from conftest import anon, anon_csrf
def test_avatar_path_traversal_denied(client):
@@ -60,6 +60,7 @@ def test_og_metadata_rejects_private_host(client):
def test_automations_require_session(client):
"""A13 : CRUD, run et press-button refusent un anonymous."""
anon(client)
anon_csrf(client)
assert client.post("/workspace/automations", json={"name": "x"}).status_code == 401
assert client.post("/workspace/automations/1/run", json={}).status_code == 401
assert client.post("/api/automations/press-button", json={}).status_code == 401
@@ -73,6 +74,7 @@ def test_outbound_webhook_requires_admin_and_public_url(client):
assert r.status_code == 400
anon(client)
anon_csrf(client)
assert client.post("/workspace/webhooks", json={"url": "https://example.com/h"}).status_code == 401
+2 -2
View File
@@ -8,7 +8,7 @@ import os
import tempfile
import pytest
from conftest import anon, login_test_client
from conftest import anon_csrf, login_test_client
from fastapi.testclient import TestClient
@@ -110,7 +110,7 @@ def test_share_invalid_permission_rejected(client):
def test_share_requires_auth(client):
anon(client)
anon_csrf(client)
pid = _make_page(client)
r = client.post(f"/api/pages/{pid}/share", json={"email": "[email protected]", "permission": "view"})
assert r.status_code == 401
+2 -2
View File
@@ -10,7 +10,7 @@ from __future__ import annotations
import secrets
import pytest
from conftest import anon
from conftest import anon, anon_csrf
from app.db import get_conn
from app.services import automations as auto_svc
@@ -114,7 +114,7 @@ def test_steps_crud_and_order(client):
def test_steps_validation_and_auth(client):
anon(client)
anon_csrf(client)
session, _ = _login(client)
aid = _mkauto(client, session)
r = client.post(f"/workspace/automations/{aid}/steps",
+3 -3
View File
@@ -11,7 +11,7 @@ import json
import secrets
import pytest
from conftest import anon
from conftest import anon_csrf
from app.db import get_conn
from app.services import calendar_sync as cal
@@ -118,7 +118,7 @@ def test_link_crud_and_encryption(client):
def test_link_validation_and_auth(client):
anon(client)
anon_csrf(client)
session, _ = _login(client)
cid = _mkcollection(client)
r = client.post("/api/v2/calendar-links",
@@ -281,7 +281,7 @@ def test_freebusy_basic(client):
def test_freebusy_validation(client):
anon(client)
anon_csrf(client)
session, _ = _login(client)
cid = _mkcollection(client)
r = client.get(f"/db/{cid}/calendar/freebusy?from=2026-10-07&to=2026-10-01",