
Percorso di attraversamento in mcp-atlassian tramite estrazione zip in upload_attachment — CVSS 9.3
Severità: Critica (CVSS 9.3)
CWE: CWE-22 — Path Traversal
Componenti interessati: sooperset/mcp-atlassian >= 0.17.0 (gemello lato lettura di GHSA-xjgw-4wvw-rgm4)
Advisory: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-27825
Il tool MCP confluence_upload_attachment passa il suo argomento file_path direttamente a open(file_path, "rb") senza alcuna validazione del percorso. Un attaccante che può invocare il tool legge file arbitrari dal filesystem del server e li esfiltra tramite upload multipart verso un endpoint Confluence controllato dall'attaccante. Il trasporto predefinito streamable-http si lega a 0.0.0.0 senza autenticazione, rendendo questo sfruttabile da remoto senza credenziali.
Questo è il gemello simmetrico lato lettura del già corretto GHSA-xjgw-4wvw-rgm4 — la correzione in v0.17.0 copriva solo il percorso di scrittura/download. Il percorso di upload è rimasto senza protezione.
mcp-atlassian aveva già ricevuto una correzione per path traversal in v0.17.0 (GHSA-xjgw-4wvw-rgm4), che ha corretto il percorso di scrittura — scaricando allegati su disco locale. La mia ipotesi: quando una correzione viene applicata a una direzione di un'operazione simmetrica, spesso l'altra direzione viene trascurata.
Ho aperto attachments.py e ho cercato tutte le chiamate open(. Il percorso di download (riga 223, 272) aveva validate_safe_path(local_path) aggiunto prima della open(). Il percorso di upload (riga 477) non ne aveva nessuna. Stesso file, stesso schema, trattamento incoerente — correzione incompleta da manuale.
La definizione del tool lo confermava: file_path: Annotated[str, Field(description="Absolute path to the file to upload")] senza vincolo pattern=, senza validatore, niente. Il campo è letteralmente documentato come accettante un percorso assoluto senza restrizioni.
L'ho riprodotto end-to-end: ho avviato il processo reale del server mcp-atlassian, l'ho pilotato con un client MCP stdio (mcp.ClientSession), l'ho puntato a uno stub HTTP Confluence locale simulato, e ho invocato confluence_upload_attachment con file_path=/etc/passwd. Il server mock ha registrato l'intero contenuto di /etc/passwd nel corpo multipart. Due esecuzioni complete di riproduzione, entrambe registrate nei file PoC.
Il binding predefinito HOST=0.0.0.0 senza autenticazione rende questo sfruttabile da remoto senza credenziali nella distribuzione predefinita.
File: src/mcp_atlassian/confluence/attachments.py, riga 477
with open(file_path, "rb") as fp: # ← file_path è controllato dall'attaccante
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
Definizione del tool (src/mcp_atlassian/servers/confluence.py:1307):
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
# Nessun pattern=, nessun validatore, nessuna validate_safe_path()
L'asimmetria con il percorso di download corretto:
# attachments.py:223 — CORRETTO (percorso di download)
validate_safe_path(local_path) # ← protezione aggiunta in v0.17.0
open(local_path, "wb")
# attachments.py:477 — VULNERABILE (percorso di upload)
open(file_path, "rb") # ← nessuna protezione, trascurato in v0.17.0
La correzione v0.17.0 per GHSA-xjgw-4wvw-rgm4 ha aggiunto chiamate validate_safe_path() sul lato scrittura (scaricando allegati su disco locale) ma non ha controllato il lato lettura (caricando file locali su Confluence). Il decorator check_write_access non è correlato — limita solo READ_ONLY_MODE.
Esposizione di rete predefinita (src/mcp_atlassian/__init__.py:151):
HOST = "0.0.0.0" # si lega a tutte le interfacce
# nessun livello di autenticazione nel trasporto streamable-http
Riprodotto completamente end-to-end contro uno stub HTTP locale che simula l'API Confluence. Vedi mcp_client.py, mock_confluence.py e poc_run1.sh.
# poc_run1.sh — legge /etc/passwd tramite confluence_upload_attachment
# 1. Avvia l'endpoint Confluence mock
python mock_confluence.py &
# 2. Invoca il tool MCP con payload di path traversal
python mcp_client.py \
--tool confluence_upload_attachment \
--page-id 123456 \
--file-path /etc/passwd \
--filename passwd.txt
# → il contenuto di /etc/passwd appare nel log di mock_confluence.py
/etc/passwd, chiavi SSH, .env, segreti applicativi)streamable-http si lega a 0.0.0.0, senza autenticazione; qualsiasi attaccante raggiungibile dalla rete può invocare direttamente i tool MCP