Aller au contenu

mcp-posture : un scanner open source pour auditer la sécurité OAuth d'un serveur MCP

mcp-posture audite un serveur MCP distant : OAuth, PKCE, TLS, tool poisoning, rug pulls. Scan passif, sortie SARIF, GitHub Action.

mcp-posture : scanner de sécurité OAuth pour serveurs MCP

En déployant des serveurs MCP en production, j'ai accumulé une liste de failles qui passaient tous les tests fonctionnels : un écran de consentement désactivé qui ouvrait la porte à un confused deputy, un chemin /mcp..%2f qui contournait l'authentification, un issuer incohérent, des métadonnées servies à la mauvaise URL. Chaque fois, la vérification tenait en quelques requêtes HTTP. J'ai donc décidé de les automatiser. Le résultat s'appelle mcp-posture, un scanner open source (licence MIT) de la posture de sécurité des serveurs MCP distants.

Pourquoi un scanner de sécurité MCP de plus ?

Plusieurs bons scanners MCP existent déjà (Snyk agent-scan, Cisco mcp-scanner, Ramparts...). Ils se concentrent surtout sur les configurations locales des clients et les serveurs stdio. Il manquait un outil qui regarde un serveur distant comme un attaquant le voit : de l'extérieur, avant de se connecter.

  • Comment le serveur challenge-t-il un appel non authentifié ?
  • Quel serveur d'autorisation désigne-t-il, et ses métadonnées sont-elles cohérentes ?
  • PKCE S256 est-il imposé ? Les flux implicit et password sont-ils désactivés ?
  • Les descriptions de tools cachent-elles des instructions ?
  • Les tools ont-ils changé depuis la dernière approbation ?

Chaque constat cite la section de la spécification concernée et la révision MCP à laquelle il s'applique, de 2025-03-26 à 2026-07-28.

Démarrage en une commande

# sans installation
uvx mcp-posture scan https://mcp.example.com/mcp

Exemple de sortie sur un serveur de démonstration volontairement mal configuré :

http://127.0.0.1:8402/mcp
  spec 2025-06-18 (negotiated) · transport streamable-http · auth not required
critical MCPP-ASM04   code_challenge_methods_supported ['plain'] does not include S256.
high     MCPP-ASM02   Metadata declares issuer 'http://127.0.0.1:8402/', expected 'http://127.0.0.1:8402'.
high     MCPP-AUTHN01 tools/list answered without credentials (2 tool(s) including delete_note).
high     MCPP-PRM06   bearer_methods_supported includes `query`.
high     MCPP-TRN08   Session IDs look sequential (numerically close across two sessions).
medium   MCPP-PRM03   resource '.../mcp/' does not match '.../mcp' (trailing slash only).

On y retrouve des erreurs bien réelles : un slash final qui suffit à casser la correspondance stricte du resource, un issuer qui diffère d'un caractère de l'URL de récupération, un tools/list accessible sans authentification qui expose un delete_note.

Ce qu'il vérifie : OAuth, TLS, tool poisoning et rug pulls

Environ 70 vérifications réparties en familles, chacune avec un identifiant stable :

  • TRN (transport) : HTTPS, version et certificat TLS, HSTS, redirection HTTP vers HTTPS, ancien transport SSE, entropie des identifiants de session.
  • AUTHN : tools/list anonyme, challenge Bearer, découverte resource_metadata, fuites d'information dans les erreurs.
  • PRM : Protected Resource Metadata (RFC 9728), correspondance exacte du resource, tokens en query string, scopes.
  • ASM : métadonnées du serveur d'autorisation (RFC 8414), égalité de l'issuer, PKCE S256 ou plain, flux implicit et password, Dynamic Client Registration ouvert, paramètre iss (RFC 9207).
  • CIMD : support des Client ID Metadata Documents, et cimd lint pour valider le document de votre propre client.
  • SCP : scopes trop larges, cohérence entre métadonnées.
  • TOOL : texte qui ressemble à des instructions (y compris en français), Unicode invisible ou bidirectionnel, blobs encodés, URLs d'exfiltration, noms confondables, shadowing d'autres tools, annotations manquantes.
  • PIN : détection des rug pulls par comparaison avec une liste de tools épinglée.
mcp-posture checks list               # le catalogue complet
mcp-posture checks show MCPP-ASM04    # justification, remédiation, références

Passif par défaut

C'était un choix de conception non négociable : un scan par défaut n'envoie que des GET de métadonnées, le handshake MCP standard, les appels de liste, et quelques sondes en lecture seule (HTTP simple, chemin inexistant, handshakes TLS). Aucun tool n'est jamais appelé.

Conséquence assumée : ce qu'il faudrait une sonde active pour confirmer (validation de l'Origin, vérification de l'audience des tokens, application effective de PKCE) est explicitement listé comme non vérifié. Je préfère un rapport qui dit ce qu'il ne sait pas à un rapport qui devine.

Un scanner qui se méfie de sa cible

Un scanner qui se connecte à des serveurs arbitraires est lui-même une surface d'attaque. mcp-posture traite chaque cible comme potentiellement hostile :

  • adresses privées, loopback et metadata cloud refusées après résolution DNS et à chaque saut de redirection (protection SSRF) ;
  • taille des réponses et décompression plafonnées ;
  • identifiants jamais transmis d'une origine à une autre ;
  • tokens lus uniquement depuis une variable d'environnement, un fichier ou stdin, et masqués partout ;
  • texte contrôlé par le serveur neutralisé dans les rapports (caractères de contrôle, bidi, invisibles) avant d'atteindre votre terminal ou vos commentaires de PR ;
  • aucune télémétrie.

Scanner ses serveurs MCP dans la CI (GitHub Action, SARIF)

Le cas d'usage qui m'intéressait le plus : vérifier en continu la liste des serveurs autorisés d'une organisation.

- uses: batou9150/mcp-posture@v1.1.1
  with:
    targets-file: mcp-servers.txt
    fail-on: high

Les résultats sortent en SARIF 2.1.0, ancrés à la ligne du fichier qui déclare chaque cible, pour un affichage direct dans GitHub code scanning. Le rapport Markdown alimente le résumé du job.

Pour la détection de rug pull, on épingle l'état approuvé, puis chaque scan compare :

mcp-posture pin https://mcp.example.com/mcp    # crée mcp-posture.lock.json

Les exceptions passent par un fichier .mcp-posture-ignore où chaque suppression exige une justification et une date d'expiration. Une exception expirée réapparaît dans le rapport.

[[ignore]]
check = "MCPP-ASM09"
target = "https://mcp.example.com/*"
justification = "DCR ouvert volontaire : registre de clients public"
expires = 2026-12-31

Pour aller plus loin

  • Scanner les serveurs de vos postes : mcp-posture discover puis scan --from-client-config auto lisent les configurations de Claude, VS Code, Cursor, Windsurf et .mcp.json.
  • Lister les tools derrière authentification : --login ouvre un flux OAuth avec PKCE dans le navigateur, sans jamais afficher le token.
  • Docker : image distroless, non-root, signée avec cosign (ghcr.io/batou9150/mcp-posture).
  • Claude Code : un plugin ajoute une revue sémantique des descriptions de tools et des snippets de remédiation pour les serveurs d'autorisation et gateways courants.
/plugin marketplace add batou9150/mcp-posture
/plugin install mcp-posture@mcp-posture

Une précision importante : mcp-posture rapporte, il ne bloque pas. Pour l'application des règles au runtime, associez-le à un proxy comme mcp-context-protector. Et ne scannez que des serveurs qui vous appartiennent ou que vous êtes autorisé à tester.

Conclusion

Chaque vérification de mcp-posture correspond à une erreur que j'ai commise ou vue en production. L'objectif est simple : qu'aucune équipe n'ait à redécouvrir ces failles à la main. Le projet est sur GitHub et PyPI, et les contributions sont bienvenues, notamment pour les vérifications actives encore en chantier. Lancez-le sur vos serveurs : le premier scan est rarement vide.

Dernier