> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Sécuriser un serveur MCP distant (OAuth) : 7 leçons apprises en production
- URL: https://www.sfeir.dev/ia/securiser-serveur-mcp-oauth/
- Published: 2026-10-13T07:59:03.000Z
- Updated: 2026-10-13T07:59:03.000Z
- Description: 7 failles OAuth rencontrées en production sur des serveurs MCP distants, invisibles aux tests fonctionnels, et comment les démontrer puis les corriger.
- Author: Baptiste PIRAULT
- Tags: IA, MCP, Agent IA, Agentic

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#s%C3%A9curiser-un-serveur-mcp-distant-oauth--7-le%C3%A7ons-apprises-en-production)

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 ?](https://www.sfeir.dev/mcp-model-context-protocol-la-cle-pour-connecter-les-ia-au-monde-reel-2/).

## Leçon 1 : confused deputy, gardez votre propre écran de consentement OAuth

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-1--confused-deputy-gardez-votre-propre-%C3%A9cran-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](https://datatracker.ietf.org/doc/html/rfc9700?ref=sfeir.dev) (§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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-2--redirect%5Furi-ce-quune-allowlist-de-redirections-prot%C3%A8ge-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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-3--path-traversal-2f-rejetez-les-chemins-non-canoniques-avant-toute-logique-de-pr%C3%A9fixe)

`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 :

```python
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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-4--rfc-9207-nommez-l%C3%A9metteur-iss-dans-la-r%C3%A9ponse-dautorisation)

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](https://datatracker.ietf.org/doc/html/rfc9207?ref=sfeir.dev) 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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-5--rfc-8414-un-seul-document-de-m%C3%A9tadonn%C3%A9es-un-seul-issuer)

La [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414?ref=sfeir.dev) (§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-configuration` si vous n'êtes pas un fournisseur OpenID. Un client qui le récupère applique le schéma OIDC et échoue sur l'absence de `jwks_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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-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

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#le%C3%A7on-7--x-mcp-header-un-param%C3%A8tre-de-tool-qui-devient-un-header-http-est-une-entr%C3%A9e-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`, `Cookie` et la famille `X-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-Method` et `Mcp-Name` sont désormais obligatoires sur les POST Streamable HTTP.

## Conclusion

[](https://github.com/batou9150/writing-workspace/blob/main/blog/drafts/1%5FMCP%2C%20s%C3%A9curiser%20un%20serveur%20distant%20%28FR%29.md?ref=sfeir.dev#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.

[Model Context Protocol (MCP) : le protocole qui connecte enfin les LLM au monde réel ?Découvrez le Model Context Protocol (MCP), un standard open source conçu pour connecter les modèles de langage (LLMs) aux données et outils du monde réel. Un protocole à la promesse ambitieuse : puissance, interopérabilité, et sécurité.![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/icon/plume-point-53fb8845-e1f1-4ca4-8bca-ff511917c51d.png)sfeir.dev - Le média incontournable pour les passionnés de tech et d'intelligence artificielleCharlotte Ryssen (Coumont)![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/thumbnail/Capture-d---e--cran-2025-07-23-a---16.25.02-d7a58099-942f-49e2-b347-c4f4a04e3cdc.png)](https://www.sfeir.dev/mcp-model-context-protocol-la-cle-pour-connecter-les-ia-au-monde-reel-2/)