NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

CVE-2026-77250: MCP Atlassian: OAuth fallback token storage writes plaintext access and refresh tokens with group-readable permissions

mediumCVSS 3.1 6.1GHSA-g5xv-mhgm-v5f6

mcp-atlassian before 0.22.0 always writes its OAuth access and refresh tokens to a plaintext file, `~/.mcp-atlassian/oauth-` plus the client ID plus `.json`, with default permissions (0664 in the reporter's test). Other local users or processes that can read the home directory can take the tokens. Upgrade to 0.22.0.

Published Sep 22, 2026 · Analysis Oct 22, 2026

What an attacker can do

OAuthConfig._save_tokens() in utils/oauth.py stores tokens in the keyring, then unconditionally keeps a plaintext copy for backwards compatibility. If the keyring write fails, the same file is the fallback. The writer makes ~/.mcp-atlassian and calls open(token_path, "w"). It sets no 0o600 mode, no chmod and no owner-only directory permission. The file holds both access_token and refresh_token, and the loader reads them back from the same path.

The reporter's safe test used a temporary HOME and mocked keyring writes. It produced a file with mode 0o664 containing both tokens.

To exploit it, an attacker needs local read access to the account's home: another user in the same group, a container sidecar, a compromised dependency, a backup job or any process with filesystem access. Because the keyring path succeeds too, the plaintext copy exists even on well-configured machines. A refresh token keeps working until it is revoked or expires, depending on the OAuth app's policy, so a copy lets the attacker keep Atlassian API access. Shared hosts, Kubernetes volumes, developer workstations and CI runners are the places this matters.

Who is affected

mcp-atlassian below 0.22.0 configured with OAuth. Installations that use API tokens or PATs only do not create this file.

Check the version and the file:

pip show mcp-atlassian
ls -l ~/.mcp-atlassian/

A mode of -rw-rw-r-- on an oauth-*.json file means group and others can read it.

mcp-atlassian

Ecosystem
PyPI
Affected
< 0.22.0
Fixed in
0.22.0
Check
pip show mcp-atlassian

How to fix it

Upgrade to 0.22.0:

pip install --upgrade mcp-atlassian==0.22.0

The advisory lists no workaround. After upgrading, delete the old oauth-*.json files and revoke and reissue the OAuth tokens for any account whose home directory other users, containers or backup tools could read. In the meantime, chmod 600 on the files and chmod 700 on ~/.mcp-atlassian limits who can read them. This is general hardening, not a step from the advisory.

How to tell if you were hit

List ~/.mcp-atlassian/oauth-*.json on every host or volume that runs the server and check their modes with ls -l. Search backups and image layers for those files. In the Atlassian admin console, review OAuth app activity and token use from unfamiliar IP addresses. A refresh token used from a host that never ran the MCP server is the key signal.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

Summary

When OAuth tokens are saved, MCP Atlassian always writes a plaintext fallback copy under ~/.mcp-atlassian/oauth-<client_id>.json. The fallback file is created with the process default umask rather than restrictive permissions. In this environment the file was created as mode 0664, exposing access and refresh tokens to same-group local users and any process that can read the home directory.

Details

OAuthConfig._save_tokens() stores OAuth token data in keyring, but it also unconditionally maintains a plaintext file fallback for backwards compatibility in src/mcp_atlassian/utils/oauth.py:350-386. If keyring saving fails it also falls back to the same file path in src/mcp_atlassian/utils/oauth.py:387-391.

The fallback writer creates ~/.mcp-atlassian and then writes oauth-<client_id>.json with a normal open(token_path, "w") call in src/mcp_atlassian/utils/oauth.py:392-420. No mode=0o600, os.open(..., 0o600), chmod, or owner-only directory permission is applied. The file contains both access_token and refresh_token (src/mcp_atlassian/utils/oauth.py:360-367) and is later loaded from the same plaintext path in src/mcp_atlassian/utils/oauth.py:450-470.

The security policy warns that OAuth client credentials and secrets should not be exposed (SECURITY.md:39-44), but the current implementation creates a persistent plaintext token copy even when keyring succeeds.

PoC

The following safe local proof uses a temporary HOME and mocked keyring writes. It creates and deletes only temporary files.

uv run python - <<'PY'
import json, os, shutil, stat, tempfile
from pathlib import Path
from unittest.mock import patch
from mcp_atlassian.utils.oauth import OAuthConfig

home = tempfile.mkdtemp(prefix='mcp-atlassian-oauth-poc-')
old_home = os.environ.get('HOME')
os.environ['HOME'] = home
try:
    cfg = OAuthConfig(client_id='poc-client', client_secret='client-secret', redirect_uri='http://localhost/callback', scope='offline_access', cloud_id='cloud-id')
    cfg.access_token = 'poc-access-token'
    cfg.refresh_token = 'poc-refresh-token'
    cfg.expires_at = 2000000000
    with patch('keyring.set_password', return_value=None):
        cfg._save_tokens()
    token_file = Path(home) / '.mcp-atlassian' / 'oauth-poc-client.json'
    mode = stat.S_IMODE(token_file.stat().st_mode)
    data = json.loads(token_file.read_text())
    print(json.dumps({
        'token_file_exists': token_file.exists(),
        'token_file_mode_octal': oct(mode),
        'contains_access_token': data.get('access_token') == 'poc-access-token',
        'contains_refresh_token': data.get('refresh_token') == 'poc-refresh-token',
        'token_file_path': str(token_file),
    }, indent=2, sort_keys=True))
finally:
    if old_home is not None:
        os.environ['HOME'] = old_home
    else:
        os.environ.pop('HOME', None)
    shutil.rmtree(home)
PY

Observed output from this environment:

{
  "contains_access_token": true,
  "contains_refresh_token": true,
  "token_file_exists": true,
  "token_file_mode_octal": "0o664",
  "token_file_path": "/tmp/mcp-atlassian-oauth-poc-9m9wvktp/.mcp-atlassian/oauth-poc-client.json"
}

The proof confirms that a plaintext file containing both access and refresh tokens is created and is not owner-only.

Impact

A local user, container sidecar, compromised dependency, backup job, or other process with filesystem read access to the account's home directory can recover OAuth access and refresh tokens. Refresh tokens can allow continued Atlassian API access until revoked or expired, depending on the OAuth app and token policy. In shared hosts, Kubernetes volumes, developer workstations, and CI runners, this can lead to persistent Atlassian account compromise.

Credited to sondt99.

Questions

Is the keyring not enough?

No. The code saves to the keyring and also keeps a plaintext file for backwards compatibility, so the file exists even when the keyring works.

Which tokens are in the file?

Both access_token and refresh_token, per the code in utils/oauth.py. The refresh token is the lasting risk.

Does this need network access?

No. The CVSS vector is local. The attacker needs read access to the home directory of the account that runs the server.

Are API-token setups affected?

The advisory concerns OAuth token saving only.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries