Comment empêcher votre serveur MCP de livrer tous vos secrets en 40 lignes de code
Un connecteur MCP, pour Model Context Protocol, permet à une application hôte, par exemple Claude, ChatGPT ou Cursor, d'utiliser une capacité externe. Ceci est réalisé par l'intermédiaire d'un serveur MCP. Cela peut consister à lire un fichier, interroger une base de données, appeler une API ou agir sur une application.
Le flux général est le suivant :
Application hôte (assistant IA : Claude, ChatGPT, Cursor…) ↓ Client MCP (composant intégré à l’application hôte) ↓ Serveur MCP (partie technique du connecteur) ↓ Système externe (fichier, base de données, API ou application)
Dans tous les cas, essentiellement, le serveur MCP ne doit jamais faire confiance automatiquement aux demandes transmises par l'application hôte ou par le modèle d'IA. Il doit contrôler chaque demande avant de l'exécuter sur le système externe.
Cas pratique : le lecteur de catalogue
Pour mieux appréhender cela, nous allons montrer comment accepter un chemin fourni par un utilisateur. Le tout en empêchant l'accès à des fichiers situés en dehors d'un dossier autorisé. Dans notre exemple, le système externe est un dossier contenant un catalogue. Le connecteur expose une opération de lecture de fichier. La protection consiste à autoriser uniquement certains fichiers, dans une zone prédéfinie, avec une taille et un format maîtrisés. À la fin, le programme acceptera "catalogue.txt", mais refusera notamment "../secret.txt", "/etc/passwd", un lien symbolique vers l'extérieur et un fichier qui n'existe pas. L'exemple est volontairement simple. Un petit programme doit lire une fiche produit placée dans le dossier "documents". Notons que cette fonction de sécurité peut ensuite être intégrée dans un serveur MCP.
Préparer le projet
Nous créons le dossier autorisé. Le dossier "documents" est la ressource externe protégée. Cette séparation matérialise le principe du moindre privilège. Le connecteur n'a besoin de lire que les fichiers de cette zone.
Pour mettre en place cela,
On copie-colle ces commandes dans PowerShell, une par une :
powershell mkdir lecteur-chemins cd lecteur-chemins mkdir documents
Notons que les commandes suivantes fonctionnent sous Linux et macOS. Sous Windows, les fichiers peuvent donc être créés avec un éditeur de texte. La logique Python reste la même. Indiquons que la version Python doit être au moins 3.10. Aucun paquet externe n'est nécessaire pour la version de démonstration.
Nous avons maintenant cette structure :
lecteur-chemins └── documents
Le fichier suivant, catalogue.txt, sert de ressource légitime. Il représente la donnée située dans le système externe. Le serveur ne doit pas supposer que toute donnée existante est automatiquement accessible. Il doit vérifier que la demande respecte la politique définie pour le connecteur. Son extension .txt sera acceptée par la politique du serveur.
Pour le créer, dans PowerShell, nous exécutons :
powershell @" Pommes - 2,50 euros Lait - 1,20 eur "@ | Set-Content -Encoding UTF8 documents\catalogue.txt
Le fichier est enregistré ici : text lecteur-chemins\documents\catalogue.txt
Définir la politique de sécurité du serveur MCP
Nous allons maintenant créer "lecteur_catalogue.py". Ce script illustre la logique de vérification qu'un serveur MCP doit appliquer. Il reçoit le nom d’un fichier, vérifie que ce nom respecte la politique de sécurité, puis affiche le contenu du fichier. Le programme ne cherchera jamais dans un dossier choisi librement par l'appelant. Sa zone autorisée sera fixée par le code ou par la configuration du serveur.
Pour exécuter un programme Python situé dans lecteur-chemins, nous ouvrons PowerShell dans ce dossier avec :
powershell cd C:\Users\admin\lecteur-chemins
Nous créons le fichier lecteur_catalogue.py dans le dossier lecteur-chemins et copions le code suivant. Les commentaires relient directement chaque instruction à la règle théorique correspondante. ROOT fixe le périmètre du système externe auquel le serveur MCP peut accéder. Si la racine était fournie dans la requête, l'appelant pourrait demander au serveur de changer lui-même sa zone de confiance.
resolve(strict=True) transforme la racine en emplacement réel et exige qu'elle existe. Le serveur échoue dès sa configuration si sa zone protégée n'est pas disponible.
EXTENSIONS_AUTORISEES constitue une liste blanche. Le serveur n'expose pas automatiquement tous les types de fichiers présents dans documents. TAILLE_MAXIMALE protège les ressources du serveur, du client MCP et du contexte de l'assistant contre une réponse excessivement volumineuse. Ces constantes appartiennent au serveur MCP, et non à l'application hôte. Elles doivent donc rester sous le contrôle de la configuration technique du connecteur. Le chemin n'est jamais ouvert avant le retour de verifier_chemin(). Cette validation est adaptée à une démonstration simple, mais une application de production doit aussi traiter les conditions de concurrence entre la validation et l'ouverture du fichier. Par exemple, un fichier pourrait être remplacé entre resolve() et read_text().Le programme renvoie aussi un message générique afin de ne pas exposer au demandeur le chemin interne du serveur.
from pathlib import Path
# La racine est choisie par le programme, jamais par l'appelant.
ROOT = (Path(__file__).parent / "documents").resolve(strict=True)
EXTENSIONS_AUTORISEES = {".txt", ".md"}
TAILLE_MAXIMALE = 1 * 1024 * 1024 # 1 Mo
class CheminRefuse(Exception):
"""Signale qu'un chemin ne respecte pas la politique de sécurité."""
def verifier_chemin(chemin_recu: str) -> Path:
"""Retourne un chemin réel autorisé ou lève CheminRefuse."""
if not isinstance(chemin_recu, str):
raise CheminRefuse("Le chemin doit être du texte")
if not chemin_recu or len(chemin_recu) > 240:
raise CheminRefuse("Chemin vide ou trop long")
if "\x00" in chemin_recu:
raise CheminRefuse("Caractère nul interdit")
# Cette politique impose des chemins relatifs de style Unix.
if "\\" in chemin_recu:
raise CheminRefuse("Séparateur non autorisé")
chemin_demande = Path(chemin_recu)
if chemin_demande.is_absolute():
raise CheminRefuse("Les chemins absolus sont interdits")
try:
# Construction sous ROOT, puis normalisation du chemin réel.
chemin_final = (ROOT / chemin_demande).resolve(strict=True)
except FileNotFoundError as erreur:
raise CheminRefuse("Fichier introuvable") from erreur
try:
# Refuse ../ et les liens symboliques qui sortent de ROOT.
chemin_final.relative_to(ROOT)
except ValueError as erreur:
raise CheminRefuse("Chemin hors de la zone autorisée") from erreur
if not chemin_final.is_file():
raise CheminRefuse("La cible n'est pas un fichier normal")
if chemin_final.suffix.lower() not in EXTENSIONS_AUTORISEES:
raise CheminRefuse("Extension interdite")
if chemin_final.stat().st_size > TAILLE_MAXIMALE:
raise CheminRefuse("Fichier trop volumineux")
return chemin_final
def lire_catalogue(chemin_recu: str) -> str:
"""Lit uniquement un fichier validé et renvoie un message générique en cas de refus."""
try:
chemin_autorise = verifier_chemin(chemin_recu)
return chemin_autorise.read_text(encoding="utf-8", errors="strict")
except CheminRefuse:
return "Lecture refusée : le chemin ne respecte pas la politique."
except UnicodeError:
return "Lecture refusée : le fichier n'est pas un texte UTF-8 valide."
except OSError:
return "Lecture impossible."
if __name__ == "__main__":
demande = input("Nom du fichier dans documents : ")
print(lire_catalogue(demande))
Exécuter le programme
Pour tester cet ensemble, nous tapons dans PowerShell : powershell python lecteur_catalogue.py
Le programme affiche :
text Nom du fichier dans documents :
Nous écrivons simplement : catalogue.txt, puis nous appuyons sur Entrée. Nous obtenons :
text Pommes - 2,50 euros Lait - 1,20 euro
Le programme ajoute automatiquement le dossier documents.
Vérifier des refus de sécurité
Le programme doit refuser certains chemins. Pour tester un fichier inexistant, nous relançons le programme :
powershell python lecteur_catalogue.py
Nous saisissons :
text inexistant.txt
Nous obtenons, de façon attendue : Lecture refusée : le chemin ne respecte pas la politique
Autre essai, nous créons un fichier test.exe dans documents, puis saisissons :
text test.exe
Le programme refuse sa lecture.
Pour tenter de sortir du dossier, nous écrivons : text ../lecteur_catalogue.py
Résultat : "Lecture refusée : le chemin ne respecte pas la politique.
Nous écrivons le chemin Windows avec antislash : text ..\lecteur_catalogue.py Le programme refuse également ce chemin.
Pour tester avec le chemin absolu Windows, nous saisissons : text C:\Windows\win.ini Le programme le refuse également.
Ce qu'il faudrait ajouter dans un vrai serveur MCP
Le code de démonstration de lecteur_catalogue.py protège le chemin et la lecture. Un connecteur MCP de production devrait également prévoir une authentification du client lorsque le transport l'exige, des permissions minimales pour le processus, une journalisation protégée, des limites de fréquence et une séparation claire entre les outils en lecture et les outils en écriture. Ces mécanismes s'ajoutent en effet au validateur, ils ne le remplacent pas. Même un client authentifié doit envoyer des paramètres contrôlés. Même une application hôte de confiance ne doit pas pouvoir imposer au serveur MCP un chemin situé hors de son périmètre.
