GitHub Advisoryadvisory · PyPI
GHSA-4596-2p6p-28cv MCP Atlassian: Insecure File Permissions on OAuth Token Storage
MEDIUM
Official page ↗
- Aliases
- CVE-2026-77268
- Ecosystem
- PyPI
- Packages
- mcp-atlassian
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Weaknesses
- CWE-732
- Reviewed
- 2026-09-22
Affected packages
• PyPI mcp-atlassian: < 0.22.0 → fixed in 0.22.0
Description
Summary
The OAuth token fallback file storage in `OAuthConfig._save_tokens_to_file()` creates token files containing access tokens, refresh tokens, and cloud IDs with default filesystem permissions (typically `0644` on Linux, world-readable). Any local user on a shared system can read these files to obtain full Atlassian API credentials, enabling unauthorized access to the victim's Jira and Confluence data.
Details
The vulnerability exists in `src/mcp_atlassian/utils/oauth.py` in the `_save_tokens_to_file` method.
Step 1 -- Directory created without restrictive permissions:
At line 402-403, the token directory is created with `mkdir(exist_ok=True)` which uses the default umask (typically creating directories with mode `0755`):
```python
src/mcp_atlassian/utils/oauth.py:402-403
token_dir = Path.home() / ".mcp-atlassian"
token_dir.mkdir(exist_ok=True)
```
Step 2 -- Token file written with default permissions:
At line 417-418, the token file containing sensitive credentials is written using `open()` with no explicit mode, inheriting default umask permissions (typically `0644` on Linux):
```python
src/mcp_atlassian/utils/oauth.py:406-418
token_path = token_dir / f"oauth-{self.client_id}.json"
if token_data is None:
token_data = {
"refresh_token": self.refresh_token,
"access_token": self.access_token,
"expires_at": self.expires_at,
"cloud_id": self.cloud_id,
"base_url": self.base_url,
}
with open(token_path, "w") as f:
json.dump(token_data, f)
```
Step 3 -- The file contains full API credentials:
The token file contains:
• `access_token`: A valid OAuth access token for the Atlassian API
• `refresh_token`: Can be exchanged for new access tokens indefinitely
• `cloud_id`: Identifies the target Atlassian Cloud instance
• `base_url`: The target Data Center instance URL
No `os.chmod` or `os.fchmod` is called anywhere after file creation.
The primary storage via `keyring` (line 373) is secure, but the fallback file storage at line 386 is always written in addition to keyring (line 386: `self._save_tokens_to_file(token_data)`). When keyring fails (common in headless/container/CI environments), the fallback becomes the only storage.
PoC
```bash
Step 1: Victim runs mcp-atlassian with OAuth and completes the flow.
This creates the token file.
Step 2: As any other user on the same system, read the token file:
cat /home/victim/.mcp-atlassian/oauth-*.json
Expected output (sensitive credentials in plaintext):
{"refresh_token": "eyJ...", "access_token": "eyJ...", "expires_at": 1741234567.0, "cloud_id": "abc-123", "base_url": null}
Step 3: Verify the token works:
curl -H "Authorization: Bearer " \
"https://api.atlassian.com/ex/jira//rest/api/3/myself"
Step 4: Use the refresh token to get a new access token:
curl -X POST "https://auth.atlassian.com/oauth/token" \
-d "grant_type=refresh_token" \
-d "client_id=" \
-d "client_secret=" \
-d "refresh_token="
```
Verify file permissions (on Linux/macOS):
```bash
ls -la ~/.mcp-atlassian/
drwxr-xr-x 2 user user 4096 Mar 10 12:00 .
-rw-r--r-- 1 user user 256 Mar 10 12:00 oauth-abc123.json
^^ ^^ ^^
world-readable!
```
Impact
• Credential theft: Any local user can read the OAuth tokens and impersonate the victim on their Atlassian Cloud/Data Center instance.
• Persistent access: The refresh token allows the attacker to generate new access tokens indefinitely, even after the original access token expires.
• Full API access: The stolen tokens grant the same API permissions as the victim, including reading/writing Jira issues, Confluence pages, and potentially sensitive project data.
• Affected environments: Shared servers, CI/CD runners, multi-user workstations, and containerized deployments where the fallback file storage is used (keyring unavailable).
Recommended Fix
1. Set restrictive permissions on the directory and file:
```python
src/mcp_atlassian/utils/oauth.py
import os
import stat
def _save_tokens_to_file(self, token_data: dict | None = None) -> None:
"""Save the tokens to a file as fallback storage."""
try:
token_dir = Path.home() / ".mcp-atlassian"
token_dir.mkdir(exist_ok=True, mode=0o700)
token_path = token_dir / f"oauth-{self.client_id}.json"
if token_data is None:
token_data = {
"refresh_token": self.refresh_token,
"access_token": self.access_token,
"expires_at": self.expires_at,
"cloud_id": self.cloud_id,
"base_url": self.base_url,
}
# Open with restrictive permissions (owner-only read/write)
fd = os.open(
str(token_path),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
stat.S_IRUSR | stat.S_IWUSR, # 0o600
)
try:
with os.fdopen(fd, "w") as f:
json.dump(token_data, f)
except Exception:
os.close(fd)
raise
logger.debug(f"Saved OAuth tokens to file {token_path} (fallback storage)")
except Exception as e:
logger.error(f"Failed to save tokens to file: {e}")
```
2. Additionally, fix the directory permissions for existing installations:
```python
In __init__ or from_env, ensure existing directories are tightened
token_dir = Path.home() / ".mcp-atlassian"
if token_dir.exists():
os.chmod(str(token_dir), 0o700)
```
Related
Alias · CVE (1)
- CVE-2026-77268 MCP Atlassian is a Model Context Protocol (MCP) server for Atlassian products (Confluence and Jira). vulnerability
Weakness · CWE (1)
- CWE-732 Incorrect Permission Assignment for Critical Resource weakness
References
- https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-4596-2p6p-28cv web
- https://github.com/sooperset/mcp-atlassian/pull/1448 web
- https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460 web
- https://github.com/sooperset/mcp-atlassian package
- https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0 web