Un serveur MCP en stdio tourne sur le poste du développeur avec ses propres identifiants. Dès qu'il passe en HTTP pour servir une équipe, il devient un service exposé sur Internet, avec un flux OAuth, des redirections et des clients que vous ne maîtrisez pas. En déployant plusieurs serveurs MCP distants, j'ai rencontré des failles qui passaient tous les tests fonctionnels. Voici les sept leçons que j'en retiens, chacune démontrable avec une simple commande. Si vous découvrez le protocole, commencez par Model Context Protocol (MCP) : le protocole qui connecte enfin les LLM au monde réel ?.
Leçon 1 : confused deputy, gardez votre propre écran de consentement OAuth
C'est la pire erreur que j'ai mise en production. Le raisonnement semblait solide :
"Le fournisseur d'identité affiche déjà un écran de consentement ; un second ajoute un clic sans ajouter de décision."
Le problème : l'écran du fournisseur consent à votre application OAuth. Il ne dit jamais quel client MCP demande l'accès, ni où le code d'autorisation sera livré. Votre propre écran est le seul endroit où ces deux informations sont liées à l'accord de l'utilisateur. Une fois cet écran désactivé et la validation des redirections trop permissive, ceci fonctionnait contre la production :
GET /mcp/authorize?client_id=<client id PUBLIC>
&redirect_uri=https://evil.example/cb&...
→ 302 directement vers le fournisseur, pas de consentement local, redirection arbitraire acceptée
Un collègue qui clique sur ce lien remet à l'attaquant un access token et un refresh token liés à sa propre identité vérifiée. C'est un confused deputy classique, décrit dans la RFC 9700 (§4.10). Et le client_id n'a rien de secret : il apparaît dans chaque redirection OAuth.
Leçon 2 : redirect_uri, ce qu'une allowlist de redirections protège vraiment
Une allowlist de redirect_uri refuse bien les endpoints contrôlés par l'attaquant (https://evil.example/cb) et les hôtes qui imitent un éditeur (vendor.example.evil.example, localhost@evil).
En revanche, elle ne peut pas isoler les tenants d'un endpoint partagé par un éditeur. Certains éditeurs publient une URL de redirection unique pour tous leurs clients ; d'autres la font varier avec un identifiant que n'importe qui peut générer en créant un connecteur. Épingler une valeur casse donc le connecteur suivant.
La règle que j'applique :
- Wildcard uniquement sur ce que l'éditeur fait réellement varier (port loopback, id de connecteur).
- Épingler tout le reste.
- Documenter que, pour les endpoints partagés, les vrais contrôles sont l'écran de consentement et PKCE.
Leçon 3 : path traversal (..%2f), rejetez les chemins non canoniques avant toute logique de préfixe
startswith("/mcp") n'est ni == "/mcp" ni startswith("/mcp/"). Sur un serveur qui servait aussi des widgets protégés, cette nuance a contourné toute l'authentification :
/widgets/business.html → 401
/mcp..%2f..%2fwidgets%2fbusiness.html → 200 ← contenu protégé, en anonyme
Le middleware d'authentification déléguait tout ce qui commençait par /mcp à la couche MCP, et la normalisation du chemin intervenait après cette décision. Le correctif qui tient rejette les chemins non canoniques avant toute comparaison de préfixe, ce qui ferme la classe de bug et pas seulement l'instance :
if ".." in path or "//" in path:
return JSONResponse({"detail": "Bad request"}, status_code=400)
if path in _DELEGATED_EXACT or path.startswith(_DELEGATED_PREFIXES): # avec slash final
return await call_next(request)Leçon 4 : RFC 9207, nommez l'émetteur (iss) dans la réponse d'autorisation
Un client qui parle à plusieurs serveurs d'autorisation peut recevoir un code émis par l'un et être piégé pour l'échanger auprès d'un autre : rien dans une redirection ?code=...&state=... ne dit qui l'a émis. La RFC 9207 ajoute un paramètre iss à la réponse d'autorisation. La révision 2026-07-28 de MCP en fait un SHOULD pour les serveurs d'autorisation et un MUST de validation pour les clients.
Si vous placez un proxy OAuth devant un fournisseur d'identité, c'est vous le serveur d'autorisation : le paramètre est à votre charge. La plupart des bibliothèques le gèrent, mais vérifiez-le. Un iss absent ne coûte rien jusqu'au jour où un client l'exige, et votre flux échoue alors à l'étape d'échange du code sans raison visible.
Leçon 5 : RFC 8414, un seul document de métadonnées, un seul issuer
La RFC 8414 (§3.3) impose que l'issuer déclaré dans les métadonnées soit identique à l'identifiant utilisé pour les récupérer. Cela interdit la "solution" évidente qui consiste à copier le document à la racine du domaine pour les clients qui l'y cherchent : la copie annonce un issuer avec chemin depuis une URL qui suggère l'origine.
- Si le chemin nu doit répondre, redirigez, ne dupliquez pas.
- Ne servez pas
/.well-known/openid-configurationsi vous n'êtes pas un fournisseur OpenID. Un client qui le récupère applique le schéma OIDC et échoue sur l'absence dejwks_uri. Mieux vaut un 404 qu'un 200 que vous n'aviez pas prévu.
Leçon 6 : comptes de service (OIDC), les appels machine restent en lecture seule
OAuth pour les humains et OIDC pour les machines sont deux chemins de confiance distincts. Un compte de service ne peut pas faire de flux navigateur : il s'authentifie avec un token OIDC et doit être autorisé via une allowlist explicite, pas via une simple vérification de domaine. Sur mes serveurs, toutes les écritures restent derrière l'authentification humaine. Donner des droits d'écriture à un appelant machine doit être une décision, pas une conséquence.
Leçon 7 : x-mcp-header, un paramètre de tool qui devient un header HTTP est une entrée non fiable
La révision 2026-07-28 a introduit x-mcp-header, qui permet de transmettre un paramètre de tool comme header HTTP vers le backend. Le besoin est réel (propager un id de tenant ou de corrélation), mais cela crée un chemin entre du texte choisi par le modèle et vos headers. Tout ce qu'un agent peut être convaincu de taper peut finir dans un header.
- Faites une allowlist des noms de headers acceptés, plutôt qu'une liste noire.
Authorization,Cookieet la familleX-Forwarded-*sont les cibles évidentes ; celles auxquelles vous n'avez pas pensé justifient l'allowlist.- Profitez-en pour vérifier que rien entre Internet et votre process ne supprime les headers
Mcp-*:Mcp-MethodetMcp-Namesont désormais obligatoires sur les POST Streamable HTTP.
Conclusion
Aucune de ces failles n'apparaît dans un test fonctionnel : le serveur répond correctement, les tools marchent, les clients se connectent. Elles apparaissent quand on regarde le serveur comme un attaquant le voit, de l'extérieur et avant authentification. Ma règle est désormais simple : chaque contrôle de sécurité doit pouvoir être démontré par une commande dont la sortie est la preuve. C'est aussi ce qui permet de les automatiser dans une CI, plutôt que de compter sur une revue manuelle avant chaque mise en production.

