PYSEC-2026-4099
Vulnerability from pysec - Published: 2026-10-01 16:38 - Updated: 2026-10-01 17:10Summary
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)callsopen(file_path, "rb")- File bytes are streamed in the multipart body of
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 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)
- Sink: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/confluence/attachments.py#L477
- Source (tool def): https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/servers/confluence.py#L1307
- Safe download comparison: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/confluence/attachments.py#L223
- Default transport bind: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/init.py#L151
Reproduction
- Start mock Confluence stub:
python mock_confluence.py(binds127.0.0.1:8443, logs all multipart bodies) - Launch MCP client against real
mcp-atlassianserver over stdio withCONFLUENCE_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,.envfiles, 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-http0.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.
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.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-httpto127.0.0.1instead of0.0.0.0, or require an auth token when bound to a non-loopback interface. - Document that
file_pathmust be confined to an operator-chosen directory and expose that directory via config.
| Name | purl | mcp-atlassian | pkg:pypi/mcp-atlassian |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mcp-atlassian",
"purl": "pkg:pypi/mcp-atlassian"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.22.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1.1",
"0.1.10",
"0.1.11",
"0.1.12",
"0.1.13",
"0.1.14",
"0.1.15",
"0.1.16",
"0.1.2",
"0.1.3",
"0.1.4",
"0.1.6",
"0.1.7",
"0.1.8",
"0.1.9",
"0.10.0",
"0.10.1",
"0.10.2",
"0.10.3",
"0.10.4",
"0.10.5",
"0.10.6",
"0.11.0",
"0.11.1",
"0.11.10",
"0.11.11",
"0.11.12",
"0.11.2",
"0.11.2a2",
"0.11.3",
"0.11.4",
"0.11.5",
"0.11.6",
"0.11.7",
"0.11.8",
"0.11.9",
"0.12.0",
"0.13.0",
"0.13.1",
"0.14.0",
"0.14.1",
"0.14.2",
"0.14.3",
"0.15.0",
"0.16.0",
"0.16.1",
"0.17.0",
"0.18.0",
"0.18.1",
"0.19.0",
"0.2.0",
"0.2.1",
"0.2.2",
"0.2.3",
"0.2.4",
"0.2.5",
"0.2.6",
"0.20.0",
"0.20.1",
"0.21.0",
"0.21.1",
"0.3.0",
"0.3.1",
"0.4.0",
"0.5.0",
"0.6.0",
"0.6.1",
"0.6.2",
"0.6.3",
"0.6.4",
"0.6.5",
"0.7.0",
"0.7.1",
"0.8.0",
"0.8.1",
"0.8.2",
"0.8.3",
"0.8.4",
"0.9.0"
]
}
],
"aliases": [
"CVE-2026-77262",
"GHSA-p6hp-93wp-fh6p"
],
"details": "## Summary\n\n`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.\n\nThis 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.\n\n## Details\n\n### Vulnerable sink\n`src/mcp_atlassian/confluence/attachments.py:477`\n```python\nwith open(file_path, \"rb\") as fp:\n files = {\"file\": (filename, fp, content_type)}\n response = self.confluence.session.post(url, files=files, ...)\n```\n`file_path` is attacker-controlled end-to-end.\n\n### Taint source\n`src/mcp_atlassian/servers/confluence.py:1290-1369`, tool definition at `:1307`:\n```python\nfile_path: Annotated[str, Field(description=\"Absolute path to the file to upload\")]\n```\nNo Pydantic `pattern=`, no validator, no `validate_safe_path()` call.\n\n### Call chain\n1. MCP client invokes `confluence_upload_attachment(page_id, file_path, ...)`\n2. Server handler forwards to `ConfluenceFetcher.upload_attachment(file_path)`\n3. `_upload_attachment_direct(file_path)` calls `open(file_path, \"rb\")`\n4. File bytes are streamed in the multipart body of `POST /wiki/rest/api/content/{page_id}/child/attachment` to the configured Confluence base URL \u2014 which the attacker also controls (they provided `CONFLUENCE_URL` via env/config or target a server they already control).\n\n### Asymmetry with the patched download path\n- `attachments.py:223` (download) \u2014 calls `validate_safe_path(local_path)` before `open(..., \"wb\")`\n- `attachments.py:272` (download) \u2014 calls `validate_safe_path(local_path)` before `open(..., \"wb\")`\n- `attachments.py:477` (upload) \u2014 **no validation**\n\nThe `check_write_access` decorator does not help: it only gates `READ_ONLY_MODE` (default `false`) and is unrelated to filesystem path safety.\n\n### Default exposure\n`src/mcp_atlassian/__init__.py:151` and `:360` \u2014 default transport is `streamable-http` binding `HOST=0.0.0.0` with no auth layer. Any network-reachable attacker can call MCP tools directly.\n\n## Severity\n\n**Primary (default `streamable-http` deployment)**\n- Vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N`\n- Score: **9.3 Critical**\n- Rationale: network-reachable, unauthenticated, scope-changed because files outside the MCP server\u0027s intended resource boundary (Confluence attachments) are exfiltrated.\n\n**Alternative (stdio-only deployment, conservative)**\n- Vector: `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N`\n- Score: **6.6 High**\n- Rationale: local attacker controlling the MCP client context.\n\nMaintainer should pick the vector that reflects the documented default deployment.\n\n## CWE\nCWE-22: Improper Limitation of a Pathname to a Restricted Directory (\u0027Path Traversal\u0027)\n\n## Affected\n- Product: `sooperset/mcp-atlassian`\n- Affected versions: **\u003e= 0.17.0, \u003c= HEAD (`d8bc78698a63cb6b321c7ca796d6329d448f7f6d`)**\n- 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`.\n- Patched versions: **none at time of disclosure**\n\n## Proof of Concept\n\nFully 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.\n\n### Permalinks (commit-pinned)\n- Sink: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/confluence/attachments.py#L477\n- Source (tool def): https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/servers/confluence.py#L1307\n- Safe download comparison: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/confluence/attachments.py#L223\n- Default transport bind: https://github.com/sooperset/mcp-atlassian/blob/d8bc78698a63cb6b321c7ca796d6329d448f7f6d/src/mcp_atlassian/__init__.py#L151\n\n### Reproduction\n1. Start mock Confluence stub: `python mock_confluence.py` (binds `127.0.0.1:8443`, logs all multipart bodies)\n2. Launch MCP client against real `mcp-atlassian` server over stdio with `CONFLUENCE_URL=http://127.0.0.1:8443`\n3. Call tool:\n```json\n{\n \"name\": \"confluence_upload_attachment\",\n \"arguments\": {\n \"page_id\": \"123456\",\n \"file_path\": \"/etc/passwd\",\n \"comment\": \"poc\"\n }\n}\n```\n\n### Run 1 \u2014 `/etc/passwd`\n- MCP response: `isError=False`, `{\"message\": \"Attachment uploaded successfully\"}`\n- Stub captured 3339-byte multipart body containing: `root:x:0:0:root:/root:/bin/bash` (and full passwd contents)\n\n### Run 2 \u2014 `/etc/hostname`\n- MCP response: `isError=False`, same success envelope\n- Stub captured 380-byte body containing: `ang3l-pc`\n\nBoth 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.\n\n## Impact\n- 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.\n- Exfiltration is covert: file bytes transit to the attacker\u0027s configured Confluence host inside a normal-looking multipart upload. No error surface; the tool returns success.\n- In the default `streamable-http` 0.0.0.0 deployment, no credentials are required.\n- Chains trivially with any AI agent that exposes this MCP server to untrusted prompt input \u2014 a prompt-injected assistant can be coerced into calling the tool with a sensitive path.\n\n## Relationship to GHSA-xjgw-4wvw-rgm4 (CVE-2026-27825)\nGHSA-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.\n\n**The upload-side counterpart at line 477 was not updated.** Same module, same maintainer, same class of bug (unchecked path \u2192 `open()`), opposite direction (read vs write). This advisory reports the incomplete-fix twin.\n\n## Remediation\n\n### Required\nCall `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.\n\n### Defense in depth\nTighten the Pydantic tool schema at `src/mcp_atlassian/servers/confluence.py:1307`:\n```python\nfile_path: Annotated[\n str,\n Field(\n description=\"Relative path within the configured upload directory\",\n pattern=r\"^(?!/)(?!.*\\.\\.)[\\w\\-./]+$\",\n ),\n]\n```\nThis blocks absolute paths and `..` at the MCP schema layer before the handler is even entered.\n\n### Additional hardening (out of scope but recommended)\n- 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.\n- Document that `file_path` must be confined to an operator-chosen directory and expose that directory via config.",
"id": "PYSEC-2026-4099",
"modified": "2026-10-01T17:10:31.244203Z",
"published": "2026-10-01T16:38:35.937260Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-p6hp-93wp-fh6p"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/pull/1448"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460"
},
{
"type": "PACKAGE",
"url": "https://github.com/sooperset/mcp-atlassian"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/mcp-atlassian"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-p6hp-93wp-fh6p"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-77262"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "MCP Atlassian: Path Traversal / Arbitrary File Read in confluence_upload_attachment MCP tool (incomplete fix of GHSA-xjgw-4wvw-rgm4)"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.