CVE-2026-77268
mediumpriority · track · 0EPSS 0.002 · 4 %ileUSEPSS · scored 2026-10-27
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 without explicit owner-only modes. Local users or processes with access through the resulting group or world permission bits can read access and refresh tokens and reuse the associated Atlassian session. The advisory traces the vulnerable input and processing flow through ~/.mcp-atlassian, oauth-<client_id>.json, access_token, and refresh_token, which identify the affected entry points, controls, and code paths. This issue is fixed in version 0.22.0.
cvss
Technical severity, 0–10. One row per scoring source.
base score 0–10 · higher = more severe · hover a vector to decode
ssvc
A stakeholder decision — act, attend, or track — from exploitation and impact.
weaknesses
The CWE weakness classes this flaw belongs to.
affected products (2)
Products and version ranges known to be affected.
advisories (2)
Vendor and ecosystem advisories describing this.
- GHSA-4596-2p6p-28cvMCP Atlassian: Insecure File Permissions on OAuth Token Storage2026-09-22ghsa · osvUS
## 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) ```
- PYSEC-2026-4082MCP Atlassian: Insecure File Permissions on OAuth Token Storage2026-10-01vendor · osvUS
## 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) ```
also known as (2)
Other names and identifiers for this entity.
sources
Which sources assert this record, and when.
references (4) nvd
External citations — patches, advisories, analysis.
- https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460Patch
- https://github.com/sooperset/mcp-atlassian/pull/1448Issue TrackingPatch
- https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0Release Notes
- https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-4596-2p6p-28cvExploitMitigationVendor Advisory
Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.
Free scan