GHSA-4596-2p6p-28cv
MCP Atlassian: Insecure File Permissions on OAuth Token Storage
Quick fix
GHSA-4596-2p6p-28cv — mcp-atlassian: upgrade to the fixed version with the command below.
pip install --upgrade 'mcp-atlassian>=0.22.0'Details
## 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 <stolen_access_token>" \ "https://api.atlassian.com/ex/jira/<stolen_cloud_id>/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=<from_env>" \ -d "client_secret=<from_env>" \ -d "refresh_token=<stolen_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) ```
Are you affected?
Enter the version of the package you're using.
Affected packages
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]