mcp-atlassian 20-GHSA cluster: a server-side-credential MCP is a confused deputy with five broken gates¶
Date reviewed: 2026-09-22 Source: GitHub advisory wave surfaced 2026-09-22T20:34–20:36Z (originally published Jul 10, 2026; all in pip/mcp-atlassian < 0.22.0, fixed 0.22.0). ~20 GHSAs on one package.
mcp-atlassian is one of the most-deployed MCP servers: it fronts Jira/Confluence with the operator's stored credentials (JIRA_USERNAME + JIRA_API_TOKEN in env — the documented quickstart "Pattern A" deployment). This wave reads like an audit report against a single threat model — who can make the server use its own credentials — and finds five independent gates, each broken differently. Every MCP server that holds server-side credentials inherits this checklist.
The five broken gates¶
| Gate | Advisory(s) | What the gate was supposed to do | Why it fails |
|---|---|---|---|
| Transport authentication | CVE-2026-77244 / GHSA-wrhw-j3f9-8vc6 (critical) | Reject requests without a valid Atlassian token | AtlassianOpaqueTokenVerifier.verify_token() accepts any non-empty string (documented as a design decision), and the OAuth-proxy auth provider is off by default — with no auth provider, FastMCP HTTP transport doesn't even challenge. No Authorization header → middleware simply doesn't set a user token → fetchers fall back to JiraConfig.from_env(), i.e. the operator's own credentials. Unauthenticated remote callers act with the operator's full Jira/Confluence access |
| Tool allowlist | CVE-2026-77243 / GHSA-3r68-hf9h-887v | ENABLED_TOOLS/TOOLSETS enforce least privilege | Filters applied in _list_tools_mcp only; _call_tool_mcp dispatches from the full 73-tool registry. Tool names are public (open source), so any direct JSON-RPC tools/call invokes write/delete tools the filter "disabled". Tellingly, READ_ONLY_MODE in the same codebase does have dual list+call enforcement — the correct pattern existed and wasn't applied |
| File-path confinement | CVE-2026-77247 / GHSA-f6pj-qv47-g96w, CVE-2026-77248 / GHSA-cc5h-2pwp-pvcc, CVE-2026-77257, CVE-2026-77259, CVE-2026-77260, CVE-2026-77255, CVE-2026-77246, CVE-2026-77253, CVE-2026-77262, CVE-2026-77265/66/69/70 | Upload tools take a file to upload, implicitly project-local | upload_attachment(file_path=…) does open(file_path,'rb') on the server's filesystem and uploads the bytes as an attachment to an issue/page the caller can read — a deterministic confused-deputy file-read oracle: the server reads what you name and hands it back to you through the Atlassian API. Chain with the auth bypass and it is unauthenticated arbitrary server-local file read (.env, kubeconfig, SSH keys). validate_safe_path() was added in a prior fix but is missing on several tool legs — same product, per-tool coverage drift |
| Containment root choice | CVE-2026-77271 / GHSA-6vmq-24h2-pj7j | The v0.17.0 traversal fix (GHSA-xjgw-4wvw-rgm4) confines writes | validate_safe_path() called without base_dir → defaults to os.getcwd() — in the standard container that's /app, the application's own source tree. Attacker overwrites a Python module inside CWD (passes validation), gets RCE on next process restart. "Patched" versions ≥0.17.0 were silently still vulnerable |
| SSRF guard | CVE-2026-77274 / GHSA-hgcf-4mq8-5266, CVE-2026-77261, CVE-2026-77267, CVE-2026-77265 (DNS rebinding), CVE-2026-77249 | validate_url_for_ssrf() blocks internal destinations | Four independent bypass legs: (1) urlparse hostname parsing diverges from the HTTP client on \@ userinfo — http://127.0.0.1:6666\@www.example.com validates against the wrong host; (2) redirect protection missing on the basic-auth and OAuth auth branches (the fix hooked one transport/session object; other branches keep an unhooked requests session); (3) DNS rebinding (validate-then-connect resolves twice); (4) an explicitly "incomplete fix" of a prior GHSA |
Also in-cluster: CVE-2026-77251 / GHSA-w66g-j6c4-hcfc — JIRA_PROJECTS_FILTER/CONFLUENCE_SPACES_FILTER are list-time-only scoping (same gate-#2 bug shape), leaking forbidden-project content; CVE-2026-77244 companion CVE-2026-77250/GHSA-g5xv-mhgm-v5f6 OAuth fallback tokens stored plaintext group-readable (local-leg harvest); CVE-2026-77272 / GHSA-g2r2-3j32-j27x reflected XSS in the OAuth setup callback handler.
Why this is durable operator guidance¶
- List-time enforcement is no enforcement. For any MCP server, gateway, or JSON-RPC surface that "disables" tools via config: enumerate the full registry from source/docs and
tools/callthe disabled names directly. The positive result class:tools/listomits the tool,tools/callreturns a success-shaped result. Same shape applies to any client-visible allowlist (menu-driven admin panels, OpenAPI-hidden endpoints,ENABLED_TOOLS, project/space filters). - "Accept any non-empty token" + "auth provider off by default" compose into unauthenticated. Test the no-header request first on every MCP HTTP/SSE/streamable-HTTP deployment — the fallback-to-env-credentials path has no error to surface. Fingerprint deployments by pattern: env-var quickstart = server-side credential deputy = highest value target on any internal network with an exposed MCP port.
- Every tool argument that names a file is a read/write sink on the server, and every upload is an exfil channel. The chain
open(caller_path)→ upload-to-attacker-readable-issue is deterministic, no prompt injection required. Sweep all upload/download/read tools for per-tool validator coverage drift — in this wave the validator existed but was missing on several legs. - Containment guards with defaulted roots enforce nothing. When a path validator's
base_dirargument defaults, find every call site that omits it;os.getcwd()= the app directory = self-overwrite = RCE-on-restart. This is the sibling of the ACE-MCP caller-supplied-root bug (Sept 15 MCP page): there the root was attacker-chosen, here it's the default — ask what root each call site actually passes. - A guard with a prior CVE is a target map. Three of these SSRF legs are literally "incomplete fix" advisories. When a product's SSRF guard has fix history, diff what each fix hooked: this wave proves the pattern where a hook (session/redirect handler/validator) is added to one auth branch and the other auth branches keep the unprotected client. Enumerate auth branches (pat / basic / oauth / header) and test each against the same owned-callback redirect chain.
urlparse≠ transport. The\@userinfo differential joins backslash/percent/slash parser-divergence fixtures on every URL-validation sweep — validate-what-the-client-connects-to, per the standing SSRF-guard rule (July 7 CKAN page).
Validation workflow (authorized scope only)¶
Owned Atlassian sandbox + disposable server
Never point a proof at a real Jira/Confluence tenant with the operator's credentials. Lab-bounded: run mcp-atlassian 0.21.x against an owned sandbox project with fake credentials in env; prove (a) no-header request reaches tools/list; (b) a filtered-out write tool answers tools/call; (c) upload_attachment with a synthetic canary file outside any app dir lands the canary content in the sandbox issue; (d) the \@ payload reaches an owned loopback callback, not metadata. For the module-overwrite leg, use a scratch copy of the package with an inert marker module — never overwrite a live install, and do not restart real services.
Report language: lead with the composed chain (no-auth transport → full registry → file-path sink → operator credentials), not the per-CVE list; state which gate each advisory breaks and what the fallback identity is.
September 23 follow-up: SDK token caches keyed wrong leak your highest-value token to your lowest-value peer¶
mcp-toolbox-sdk-pythontoolbox-coremodule-level ID-token cache not keyed by audience (CVE-2026-19202, GHSA-jcjp-gr26-563j): the Google ID token minted for Service A is cached process-globally and replayed to Service B when the same process authenticates to multiple audiences. Whoever operates, compromises, or passively observes traffic to the lower-trust Service B captures a valid token for Service A and replays it to impersonate the application there. Operator axes for any agent/MCP/SDK that fetches cloud identity tokens: (1) sweep credential caches for key shape — a cache keyed by nothing, by client id, or by timestamp instead of (audience, scope) will cross-serve tokens; (2) in recon, treat the lowest-trust peer an SDK talks to as a token-capture position for its highest-trust peers — enumerate which audiences a target process shares one SDK/process with; (3) token theft here needs no exploit, only a listening socket on a service the victim library also calls. Proof in labs = a fake "Service B" that logs receivedAuthorizationheaders while a test client legitimately fetches a real Service-A token; never target live Google endpoints.
Tracked, not published¶
- OAuth setup callback reflected XSS (77272) — low-severity stored/reflected class, no new axis beyond existing OAuth-CSRF family.
- Plaintext/group-readable token stores (77250, 77268) — local-harvest class, same family as the
/proc/cmdlineargv item folded the same evening. mppxgas-draining pair, Home Assistant Statistics-Graph XSS, Cloudreve quota TOCTOU, Spree cart-associate IDOR, Nautobot self-approval, Novujavascript:inbox redirect, Graylog saved-search priv-esc — single-axis product items on already-catalogued families.