
Preuve de concept d’exploitation pour une lecture arbitraire de fichier dans mcp-atlassian via un traversement de chemin dans confluence_upload_attachment, avec analyse et scripts de reproduction.
Sévérité : Élevée (CVSS 8.6)
CWE : CWE-22 — Traversée de chemin
Version affectée : sooperset/mcp-atlassian < 0.22.0
Corrigé dans : 0.22.0
Avis de sécurité : GHSA-p6hp-93wp-fh6p
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-77262
Crédit : Romain Deperne
L'outil MCP confluence_upload_attachment transmet son argument file_path directement à open(file_path, "rb") sans aucune validation de chemin. Un attaquant capable d'invoquer l'outil peut lire des fichiers arbitraires du système de fichiers du serveur et les exfiltrer via un téléversement multipart vers un point de terminaison Confluence contrôlé par l'attaquant. Le transport par défaut streamable-http écoute sur sans authentification, ce qui rend cette vulnérabilité exploitable à distance sans identifiants.
0.0.0.0Il s'agit du jumeau symétrique côté lecture de l'avis GHSA-xjgw-4wvw-rgm4 précédemment corrigé — le correctif de la v0.17.0 ne couvrait que le chemin d'écriture/téléchargement. Le chemin de téléversement est resté sans protection.
mcp-atlassian avait déjà reçu un correctif de traversée de chemin dans la v0.17.0 (GHSA-xjgw-4wvw-rgm4), qui corrigeait le chemin d'écriture — le téléchargement de pièces jointes sur le disque local. Mon hypothèse : lorsqu'un correctif est appliqué à une direction d'une opération symétrique, l'autre direction est souvent oubliée.
L'examen des sites d'appel de open( dans attachments.py a montré que le chemin de téléchargement appelait validate_safe_path(local_path) avant d'ouvrir un fichier, alors que le chemin de téléversement ne le faisait pas. Les deux directions présentaient une validation de chemin incohérente.
La définition de l'outil l'a confirmé : file_path: Annotated[str, Field(description="Absolute path to the file to upload")] sans contrainte pattern=, sans validateur, rien. Le champ est littéralement documenté comme acceptant un chemin absolu sans aucune restriction.
Je l'ai reproduit de bout en bout : j'ai lancé le processus serveur réel mcp-atlassian, je l'ai piloté avec un client MCP stdio (mcp.ClientSession), je l'ai pointé vers un stub HTTP Confluence simulé local, et j'ai invoqué confluence_upload_attachment avec file_path=/etc/passwd. Le serveur simulé a consigné le contenu complet de /etc/passwd dans le corps multipart. Deux exécutions complètes de reproduction, toutes deux consignées dans les fichiers PoC.
La liaison par défaut HOST=0.0.0.0 sans authentification rend cette vulnérabilité exploitable à distance sans identifiants dans le déploiement par défaut.
Fichier : src/mcp_atlassian/confluence/attachments.py, ligne 477
with open(file_path, "rb") as fp: # ← file_path est contrôlé par l'attaquant
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
Définition de l'outil (src/mcp_atlassian/servers/confluence.py:1307) :
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
# Pas de pattern=, pas de validateur, pas de validate_safe_path()
L'asymétrie avec le chemin de téléchargement corrigé :
# attachments.py:223 — CORRIGÉ (chemin de téléchargement)
validate_safe_path(local_path) # ← protection ajoutée dans la v0.17.0
open(local_path, "wb")
# attachments.py:477 — VULNÉRABLE (chemin de téléversement)
open(file_path, "rb") # ← aucune protection, oublié dans la v0.17.0
Le correctif de la v0.17.0 pour GHSA-xjgw-4wvw-rgm4 a ajouté des appels validate_safe_path() côté écriture (téléchargement de pièces jointes sur le disque local) mais n'a pas audité le côté lecture (téléversement de fichiers locaux vers Confluence). Le décorateur check_write_access n'est pas concerné — il ne contrôle que READ_ONLY_MODE.
Exposition réseau par défaut (src/mcp_atlassian/__init__.py:151) :
HOST = "0.0.0.0" # écoute sur toutes les interfaces
# aucune couche d'authentification dans le transport streamable-http
Reproduction complète de bout en bout contre un stub HTTP local simulant l'API Confluence. Voir mcp_client.py, mock_confluence.py et poc_run1.sh.
# poc_run1.sh — lit /etc/passwd via confluence_upload_attachment
# 1. Démarrer le point de terminaison Confluence simulé
python mock_confluence.py &
# 2. Invoquer l'outil MCP avec la charge utile de traversée de chemin
python mcp_client.py \
--tool confluence_upload_attachment \
--page-id 123456 \
--file-path /etc/passwd \
--filename passwd.txt
# → le contenu de /etc/passwd apparaît dans le journal de mock_confluence.py
/etc/passwd, clés SSH, .env, secrets d'application)streamable-http écoute sur 0.0.0.0, sans authentification ; tout attaquant joignable sur le réseau peut invoquer directement les outils MCP