
Path Traversal en mcp-atlassian mediante extracción de zip en upload_attachment — CVSS 9.3
Severidad: Crítica (CVSS 9.3)
CWE: CWE-22 — Path Traversal
Afectado: sooperset/mcp-atlassian >= 0.17.0 (gemelo del lado de lectura de GHSA-xjgw-4wvw-rgm4)
Aviso: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-27825
La herramienta MCP confluence_upload_attachment pasa su argumento file_path directamente a open(file_path, "rb") sin validación de ruta. Un atacante que pueda invocar la herramienta lee archivos arbitrarios del sistema de archivos del servidor y los exfiltra mediante una carga multipart a un endpoint de Confluence controlado por el atacante. El transporte predeterminado streamable-http se vincula a 0.0.0.0 sin autenticación, lo que lo hace explotable de forma remota sin credenciales.
Este es el gemelo simétrico del lado de lectura del previamente parcheado GHSA-xjgw-4wvw-rgm4 — la corrección de v0.17.0 solo cubrió la ruta de escritura/descarga. La ruta de carga quedó sin protección.
mcp-atlassian ya había recibido una corrección de path traversal en v0.17.0 (GHSA-xjgw-4wvw-rgm4), que parcheó la ruta de escritura — descargar adjuntos al disco local. Mi hipótesis: cuando se aplica una corrección a una dirección de una operación simétrica, a menudo se pasa por alto la otra dirección.
Abrí attachments.py y busqué todas las llamadas a open(. La ruta de descarga (línea 223, 272) tenía validate_safe_path(local_path) añadido antes del open(). La ruta de carga (línea 477) no tenía ninguna. Mismo archivo, mismo patrón, tratamiento inconsistente — corrección incompleta de manual.
La definición de la herramienta lo confirmó: file_path: Annotated[str, Field(description="Absolute path to the file to upload")] sin restricción pattern=, sin validador, sin nada. El campo está documentado literalmente como aceptando una ruta absoluta sin restricción.
Lo reproduje de extremo a extremo: inicié el proceso real del servidor mcp-atlassian, lo conduje con un cliente MCP stdio (mcp.ClientSession), lo apunté a un stub HTTP local simulado de Confluence, e invoqué confluence_upload_attachment con file_path=/etc/passwd. El servidor simulado registró el contenido completo de /etc/passwd en el cuerpo multipart. Dos ejecuciones completas de reproducción, ambas registradas en los archivos PoC.
El enlace predeterminado HOST=0.0.0.0 sin autenticación hace que esto sea explotable de forma remota sin credenciales en la implementación predeterminada.
Archivo: src/mcp_atlassian/confluence/attachments.py, línea 477
with open(file_path, "rb") as fp: # ← file_path está controlado por el atacante
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
Definición de la herramienta (src/mcp_atlassian/servers/confluence.py:1307):
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
# Sin pattern=, sin validador, sin validate_safe_path()
La asimetría con la ruta de descarga parcheada:
# attachments.py:223 — PARCHEADO (ruta de descarga)
validate_safe_path(local_path) # ← protección añadida en v0.17.0
open(local_path, "wb")
# attachments.py:477 — VULNERABLE (ruta de carga)
open(file_path, "rb") # ← sin protección, omitido en v0.17.0
El parche de v0.17.0 para GHSA-xjgw-4wvw-rgm4 añadió llamadas a validate_safe_path() en el lado de escritura (descargar adjuntos al disco local) pero no auditó el lado de lectura (cargar archivos locales a Confluence). El decorador check_write_access no está relacionado — solo controla READ_ONLY_MODE.
Exposición de red predeterminada (src/mcp_atlassian/__init__.py:151):
HOST = "0.0.0.0" # se vincula a todas las interfaces
# sin capa de autenticación en el transporte streamable-http
Reproducido completamente de extremo a extremo contra un stub HTTP local que simula la API de Confluence. Ver mcp_client.py, mock_confluence.py y poc_run1.sh.
# poc_run1.sh — lee /etc/passwd mediante confluence_upload_attachment
# 1. Iniciar endpoint simulado de Confluence
python mock_confluence.py &
# 2. Invocar herramienta MCP con payload de path traversal
python mcp_client.py \
--tool confluence_upload_attachment \
--page-id 123456 \
--file-path /etc/passwd \
--filename passwd.txt
# → el contenido de /etc/passwd aparece en el registro de mock_confluence.py
/etc/passwd, claves SSH, .env, secretos de aplicación)streamable-http se vincula a 0.0.0.0, sin autenticación; cualquier atacante con acceso a la red puede invocar herramientas MCP directamente