From fae335f8084e5e8107e5170a07aa96638966ff99 Mon Sep 17 00:00:00 2001 From: Spark Date: Thu, 22 Oct 2026 08:42:42 -0400 Subject: [PATCH] =?UTF-8?q?7.76.1=20=E2=80=94=20correctif=20:=20/mcp=20exe?= =?UTF-8?q?mpt=C3=A9=20du=20CSRF=20(client=20Bearer=20machine-=C3=A0-machi?= =?UTF-8?q?ne)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le middleware CSRF rejetait tout POST /mcp en 403 avant la route ; les tests passaient car leur client porte session + CSRF. Test de régression : client anonyme Bearer seul (200 avec jeton, 401 sans). --- VERSION | 2 +- app/middleware/csrf.py | 7 ++++++- tests/test_fusion_mcp_server.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index d7e7a34..4fdb6e9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.76.0 +7.76.1 diff --git a/app/middleware/csrf.py b/app/middleware/csrf.py index 8926a33..38d3a6c 100644 --- a/app/middleware/csrf.py +++ b/app/middleware/csrf.py @@ -20,6 +20,11 @@ class CSRFMiddleware(BaseHTTPMiddleware): SAFE_METHODS = {"GET", "HEAD", "OPTIONS"} # NOTE: ``/scim/v2`` is Bearer-token only (no cookie auth), so CSRF does not # apply — IdP SCIM clients (Okta, Entra) cannot send an X-CSRF-Token. + # Même logique pour ``/mcp`` (Fusion, 7.76.1) : le serveur MCP est + # machine-à-machine, authentifié par les jetons d'API personnels + # (Bearer) — le service d'agents ne peut pas envoyer de X-CSRF-Token. + # Sans cette exemption, tout POST /mcp mourait en 403 avant la route + # (les tests passaient : leur client porte session + CSRF). # A19 — LISTE FINALE : plus aucun préfixe cookie-auth n'est exempté. # Tous les appels non-GET du front envoient désormais `X-CSRF-Token` # (46 sites équipés en v7.3.6 : agent_panel, settings, local_workspace, @@ -30,7 +35,7 @@ class CSRFMiddleware(BaseHTTPMiddleware): # - publics : /s/ (sites), /f/ (forms) # - infra/diag : /api/csrf-token (le jeton lui-même), /api/frontend-error EXCLUDED_PATHS = { - "/api/webhook", "/api/v1", "/api/v2", "/scim/v2", + "/api/webhook", "/api/v1", "/api/v2", "/scim/v2", "/mcp", "/auth/callback", "/auth/register", "/auth/local-login", "/auth/local-verify", "/auth/2fa", "/auth/saml", "/auth/oidc", "/auth/webauthn", "/s/", "/f/", "/api/csrf-token", "/api/frontend-error", diff --git a/tests/test_fusion_mcp_server.py b/tests/test_fusion_mcp_server.py index afffd40..e73dda4 100644 --- a/tests/test_fusion_mcp_server.py +++ b/tests/test_fusion_mcp_server.py @@ -156,6 +156,21 @@ def test_mcp_ecriture_sans_jeton_401(client, donnees): assert r.status_code == 401 +def test_mcp_client_bearer_sans_session_ni_csrf(client, jeton, donnees): + """Régression 7.76.1 : un vrai client MCP est machine-à-machine + (Bearer seul, ni session ni jeton CSRF) — exactement le service + d'agents. Le client de test habituel porte une session + un CSRF, + ce qui masquait le 403 du middleware en production.""" + from tests.conftest import anon + anon(client) + r = _rpc(client, jeton, 60, "initialize", {}) + assert r.status_code == 200, r.text + cols = _appel(client, jeton, 61, "list_collections") + assert any(c["name"] == "Projet Alpha" for c in cols) + r = _rpc(client, None, 62, "tools/list") + assert r.status_code == 401, r.text + + def test_mcp_info_get(client, jeton): r = client.get("/mcp") assert r.status_code == 200