[mcp-atlassian] Authentication bypass in HTTP transport: AtlassianOpaqueTokenVerifier accepts any non-empty tokenGHSA-wrhw-j3f9-8vc6
CRITICALFix: sooperset/mcp-atlassian#1448GHSA-wrhw-j3f9-8vc6 is a critical-severity (CVSS 10) Improper Authentication vulnerability in mcp-atlassian. A fix is available for mcp-atlassian — see the affected versions and patch details below.
Exploitation Status
No confirmed exploitation observed yet
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-wrhw-j3f9-8vc6.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-wrhw-j3f9-8vc6 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
mcp-atlassianReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Description
mcp-atlassian deploys in two common patterns:
Pattern A (single-user, server-side credentials): operator sets JIRA_USERNAME + JIRA_API_TOKEN (or CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN) in environment variables. Server uses these to call Jira/Confluence. This is the documented quickstart pattern.
Pattern B (multi-user, OAuth or per-request PAT): operator sets up OAuth proxy or accepts per-user tokens via Authorization or service headers.
The authentication mechanism in HTTP transport has two issues that combine to permit unauthenticated access to Pattern A deployments:
-
AtlassianOpaqueTokenVerifier.verify_token() at
src/mcp_atlassian/utils/token_verifier.pyaccepts any non-empty string as a valid token:async def verify_token(self, token: str) -> AccessToken | None: if not token: return None scopes = self.required_scopes or [] return AccessToken( token=token, client_id="atlassian", scopes=scopes, expires_at=int(time.time()) + 86400 * 30, )The docstring documents this: "we accept non-empty tokens and attach the required scopes."
-
The default deployment does NOT enable the OAuth proxy auth provider (OAUTH_PROXY_ENABLE_ENV defaults to false; main.py:726). When
_build_auth_provider()returns None, FastMCP HTTP transport accepts requests with no authentication challenge. -
UserTokenMiddleware._parse_auth_header(main.py:601-664) extracts tokens from Authorization headers and stores them in scope state. If NO Authorization header is present (main.py:584-595), the middleware does not reject the request — it simply does not populateuser_atlassian_token. -
JiraFetcher / ConfluenceFetcher fall back to
JiraConfig.from_env()when no user-supplied token is in scope state.from_env()readsJIRA_API_TOKENandJIRA_USERNAMEfrom environment and uses them as the API credentials.
Composition: an attacker who reaches the HTTP transport (e.g., server exposed on a port reachable from attacker — direct bind, Docker port mapping, reverse proxy without auth, container in a network the attacker joined) can:
- Send no Authorization header at all, OR
- Send any garbage Bearer token
Either request reaches tool handlers. The tool handlers, finding no user-supplied token, use the server's env-var credentials to call Jira / Confluence. The attacker has full operator-level access to the operator's Atlassian instance.
This is the same vulnerability class as CVE-2026-27825 (Arctic Wolf, unauthenticated RCE+SSRF in Atlassian MCP). The previous CVE was for a different code path; this report concerns the auth verifier and middleware behavior present in the current main branch.
**Steps to Reproduce**
Source-level demonstration:
1. Verify the verifier accepts arbitrary tokens:
cd src/
python -c "
import asyncio
from mcp_atlassian.utils.token_verifier import AtlassianOpaqueTokenVerifier
v = AtlassianOpaqueTokenVerifier(required_scopes=['read:jira-work'])
result = asyncio.run(v.verify_token('anything-at-all'))
print('Accepted:', result is not None)
print('Token stored:', result.token if result else None)
print('Scopes granted:', result.scopes if result else None)
"
Expected:
Accepted: True
Token stored: anything-at-all
Scopes granted: ['read:jira-work']
End-to-end (researcher's own Atlassian sandbox):
1. Start mcp-atlassian in HTTP mode against a researcher-owned Atlassian
Cloud instance with JIRA_API_TOKEN configured:
export JIRA_URL=https://researcher.atlassian.net
export [email protected]
export JIRA_API_TOKEN=<researcher's-real-token>
export MCP_TRANSPORT=streamable-http
export PORT=3000
# Do NOT set OAUTH_PROXY_ENABLE_ENV — leave it default (false)
mcp-atlassian
2. From another machine (or curl on localhost), with no auth:
curl -X POST http://localhost:3000/mcp \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0", "id":1, "method":"tools/call",
"params":{
"name":"jira_get_issue",
"arguments":{"issue_key":"PROJ-1"}
}
}'
Expected: returns the Jira issue payload — using the server's
JIRA_API_TOKEN to authenticate to Atlassian. No client-side token
provided.
3. Optional: same call with a garbage Bearer for completeness:
curl ... -H "Authorization: Bearer anything-at-all" ...
Same result.
**Impact**:
Attacker profile: any party with network reach to the HTTP transport.
No credentials, no prior account, no privileged position required.
Typical deployment patterns at risk:
- Docker compose with port exposed (very common in mcp-atlassian's
docs and community deployments)
- Cloud-deployed MCP server behind a load balancer where the LB
doesn't enforce auth (delegates to the application)
- Internal corporate network where any employee can reach the server
- Misconfigured Kubernetes ingress
- Tunneled MCP server via ngrok / Cloudflare Tunnel for development
that gets left exposed
Security impact after exploitation:
1. Full Jira read access. Every project, every issue, every comment,
every attachment, every user — using the operator's API token.
2. Full Jira write access. Create, edit, delete issues. Add comments
under the operator's identity. Move issues across boards. Bulk-edit.
3. Full Confluence read/write access. Same surface — pages, spaces,
attachments, permissions, restricted spaces visible to the operator's
identity.
4. Audit trail names the operator. Every API call is signed with the
operator's token. From Atlassian's logging side, the operator is the
actor — covering the attacker's tracks and shifting blame.
5. Pivot. Attachments often contain credentials, infrastructure
diagrams, customer data. Confluence pages often store secrets in
plaintext under the assumption of access control.
6. Persistence. Attacker can create new Jira webhooks, automation rules,
or Confluence integrations that survive beyond the MCP session.
CVE-2026-27825 (Arctic Wolf, May 2026) was scored CVSS 9.8 Critical for
unauth RCE+SSRF in this same code surface. This report is the auth-bypass
component of the same class against the current main branch.
**Suggested Fix**
The most direct fix is the standard MCP-server-with-env-creds pattern:
1. When OAUTH_PROXY_ENABLE_ENV is not set, REFUSE to start the HTTP
transport unless an explicit "single-user mode" flag is set:
SINGLE_USER_MODE = is_env_truthy("MCP_ATLASSIAN_SINGLE_USER")
if MCP_TRANSPORT == "streamable-http" and not auth_provider and not SINGLE_USER_MODE:
raise SystemExit(
"HTTP transport requires either OAUTH_PROXY_ENABLE=true "
"or MCP_ATLASSIAN_SINGLE_USER=true (acknowledges that env "
"credentials will be used for any incoming request)."
)
2. Even with SINGLE_USER_MODE, bind the HTTP transport to 127.0.0.1
by default unless the operator overrides with an explicit
MCP_ATLASSIAN_BIND_PUBLIC=true.
3. Document the multi-tenant pattern as requiring OAuth proxy or
per-request user-token middleware with a verifier that actually
verifies (not the opaque-accept-anything stub).
4. Replace AtlassianOpaqueTokenVerifier with a verifier that performs
a token-info or whoami call to Atlassian. The fact that Atlassian
tokens are opaque does not preclude verification — a
/rest/api/3/myself call validates the token and returns the
associated user, which the verifier can attach to the AccessToken's
scopes and user_id fields.
Defense in depth: the README quickstart should not encourage exposing
the HTTP transport without auth. The docker-compose.yml in the repo
should bind to 127.0.0.1 only by default.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | mcp-atlassian | all versions | 0.22.0pip install --upgrade 'mcp-atlassian==0.22.0' |
Affected Products
mcp atlassianmcp-atlassianDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for mcp-atlassian, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update mcp-atlassian to 0.22.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wrhw-j3f9-8vc6 is resolved across your whole dependency graph.
Workarounds
Restrict outbound requests from the affected component to an allowlist of hosts, block access to link-local and internal address ranges at the network layer, and require authentication on internal services so a forged request cannot reach them unauthenticated.
Frequently Asked Questions
Is GHSA-wrhw-j3f9-8vc6 in your dependencies?
Find it across PyPI, including transitive dependencies.