PT-2026-105847 · Pypi · Mcp-Attlasian

Published

2026-10-01

·

Updated

2026-10-01

CVSS v3.1

6.1

Medium

VectorAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

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.
bash
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:
json
{
 "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.

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Related Identifiers

PYSEC-2026-4094

Affected Products

Mcp-Attlasian