PYSEC-2026-4082

vendorUSosv

MCP Atlassian: Insecure File Permissions on OAuth Token Storage

Advisory ansehen veröffentlicht 2026-10-01Lizenz: CC-BY-4.0

## 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) ```

01

beschreibt (1)

Zugehörige CVEs.

IDSchweregrad ausnutzbar
CVE-2026-77268US——
MCP Atlassian is a Model Context Protocol (MCP) server for Atlassian products (Confluence and Jira). Prior to 0.22.0, the OAuth fallback token directory and JSON file are created w…
02

Referenzen

Externe Belege — Patches, Meldungen, Analysen.

scry

Advisories wie diese benennen die exponierten Produkte, die Angreifer auf Ihrer Angriffsfläche ins Visier nehmen. Sehen Sie mit einem kostenlosen Passiv-Scan, was exponiert ist.

Kostenloser Scan