
Traversée de chemin dans mcp-atlassian via l'extraction de zip dans upload_attachment — CVSS 9.3
Sévérité : Critique (CVSS 9.3)
CWE : CWE-22 — Traversée de chemin
Composant affecté : sooperset/mcp-atlassian >= 0.17.0 (jumeau côté lecture de GHSA-xjgw-4wvw-rgm4)
Avis de sécurité : GHSA
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-27825
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 lit des fichiers arbitraires du système de fichiers du serveur et les exfiltre via un téléversement multipart vers un point de terminaison Confluence contrôlé par l'attaquant. Le transport par défaut streamable-http se lie à 0.0.0.0 sans authentification, ce qui rend cette vulnérabilité exploitable à distance sans identifiants.
Il 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 des 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.
J'ai ouvert attachments.py et recherché tous les appels open(. Le chemin de téléchargement (lignes 223, 272) avait validate_safe_path(local_path) ajouté avant le open(). Le chemin de téléversement (ligne 477) n'en avait aucun. Même fichier, même modèle, traitement incohérent — correctif incomplet par excellence.
La définition de l'outil le confirmait : 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 journalisé le contenu complet de /etc/passwd dans le corps multipart. Deux exécutions complètes de reproduction, toutes deux journalisé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 des pièces jointes sur le disque local) mais n'a pas audité le côté lecture (téléversement des 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" # se lie à toutes les interfaces
# aucune couche d'authentification dans le transport streamable-http
Reproduit intégralement 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 se lie à 0.0.0.0, sans authentification ; tout attaquant joignable sur le réseau peut invoquer directement les outils MCP