PT-2026-105847 · Pypi · Mcp-Attlasian
Publicado
2026-10-01
·
Atualizado
2026-10-01
CVSS v3.1
6.1
Média
| Vetor | AV: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)
PYObserved 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.
Correção
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Identificadores relacionados
Produtos afetados
Mcp-Attlasian