{"id":"CVE-2026-77268","aliases":["GHSA-4596-2p6p-28cv","PYSEC-2026-4082"],"url":"https://o3.security/vulnerability/CVE-2026-77268","summary":"MCP Atlassian: Insecure File Permissions on OAuth Token Storage","details":"## Summary\n\nThe 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.\n\n## Details\n\nThe vulnerability exists in `src/mcp_atlassian/utils/oauth.py` in the `_save_tokens_to_file` method.\n\n**Step 1 -- Directory created without restrictive permissions:**\n\nAt line 402-403, the token directory is created with `mkdir(exist_ok=True)` which uses the default umask (typically creating directories with mode `0755`):\n\n```python\n# src/mcp_atlassian/utils/oauth.py:402-403\ntoken_dir = Path.home() / \".mcp-atlassian\"\ntoken_dir.mkdir(exist_ok=True)\n```\n\n**Step 2 -- Token file written with default permissions:**\n\nAt 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):\n\n```python\n# src/mcp_atlassian/utils/oauth.py:406-418\ntoken_path = token_dir / f\"oauth-{self.client_id}.json\"\n\nif token_data is None:\n    token_data = {\n        \"refresh_token\": self.refresh_token,\n        \"access_token\": self.access_token,\n        \"expires_at\": self.expires_at,\n        \"cloud_id\": self.cloud_id,\n        \"base_url\": self.base_url,\n    }\n\nwith open(token_path, \"w\") as f:\n    json.dump(token_data, f)\n```\n\n**Step 3 -- The file contains full API credentials:**\n\nThe token file contains:\n- `access_token`: A valid OAuth access token for the Atlassian API\n- `refresh_token`: Can be exchanged for new access tokens indefinitely\n- `cloud_id`: Identifies the target Atlassian Cloud instance\n- `base_url`: The target Data Center instance URL\n\n**No `os.chmod` or `os.fchmod` is called** anywhere after file creation.\n\nThe 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.\n\n## PoC\n\n```bash\n# Step 1: Victim runs mcp-atlassian with OAuth and completes the flow.\n# This creates the token file.\n\n# Step 2: As any other user on the same system, read the token file:\ncat /home/victim/.mcp-atlassian/oauth-*.json\n\n# Expected output (sensitive credentials in plaintext):\n# {\"refresh_token\": \"eyJ...\", \"access_token\": \"eyJ...\", \"expires_at\": 1741234567.0, \"cloud_id\": \"abc-123\", \"base_url\": null}\n\n# Step 3: Verify the token works:\ncurl -H \"Authorization: Bearer <stolen_access_token>\" \\\n  \"https://api.atlassian.com/ex/jira/<stolen_cloud_id>/rest/api/3/myself\"\n\n# Step 4: Use the refresh token to get a new access token:\ncurl -X POST \"https://auth.atlassian.com/oauth/token\" \\\n  -d \"grant_type=refresh_token\" \\\n  -d \"client_id=<from_env>\" \\\n  -d \"client_secret=<from_env>\" \\\n  -d \"refresh_token=<stolen_refresh_token>\"\n```\n\n**Verify file permissions (on Linux/macOS):**\n\n```bash\nls -la ~/.mcp-atlassian/\n# drwxr-xr-x  2 user user 4096 Mar 10 12:00 .\n# -rw-r--r--  1 user user  256 Mar 10 12:00 oauth-abc123.json\n#                ^^ ^^ ^^\n#                world-readable!\n```\n\n## Impact\n\n- **Credential theft**: Any local user can read the OAuth tokens and impersonate the victim on their Atlassian Cloud/Data Center instance.\n- **Persistent access**: The refresh token allows the attacker to generate new access tokens indefinitely, even after the original access token expires.\n- **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.\n- **Affected environments**: Shared servers, CI/CD runners, multi-user workstations, and containerized deployments where the fallback file storage is used (keyring unavailable).\n\n## Recommended Fix\n\n**1. Set restrictive permissions on the directory and file:**\n\n```python\n# src/mcp_atlassian/utils/oauth.py\n\nimport os\nimport stat\n\ndef _save_tokens_to_file(self, token_data: dict | None = None) -> None:\n    \"\"\"Save the tokens to a file as fallback storage.\"\"\"\n    try:\n        token_dir = Path.home() / \".mcp-atlassian\"\n        token_dir.mkdir(exist_ok=True, mode=0o700)\n\n        token_path = token_dir / f\"oauth-{self.client_id}.json\"\n\n        if token_data is None:\n            token_data = {\n                \"refresh_token\": self.refresh_token,\n                \"access_token\": self.access_token,\n                \"expires_at\": self.expires_at,\n                \"cloud_id\": self.cloud_id,\n                \"base_url\": self.base_url,\n            }\n\n        # Open with restrictive permissions (owner-only read/write)\n        fd = os.open(\n            str(token_path),\n            os.O_WRONLY | os.O_CREAT | os.O_TRUNC,\n            stat.S_IRUSR | stat.S_IWUSR,  # 0o600\n        )\n        try:\n            with os.fdopen(fd, \"w\") as f:\n                json.dump(token_data, f)\n        except Exception:\n            os.close(fd)\n            raise\n\n        logger.debug(f\"Saved OAuth tokens to file {token_path} (fallback storage)\")\n    except Exception as e:\n        logger.error(f\"Failed to save tokens to file: {e}\")\n```\n\n**2. Additionally, fix the directory permissions for existing installations:**\n\n```python\n# In __init__ or from_env, ensure existing directories are tightened\ntoken_dir = Path.home() / \".mcp-atlassian\"\nif token_dir.exists():\n    os.chmod(str(token_dir), 0o700)\n```","published":"2026-09-22T18:44:46.039Z","modified":"2026-10-21T02:59:48.008403136Z","cvss":{"score":5.5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"mcp-atlassian","fixedVersion":"0.22.0"}],"fix":{"url":"https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460","label":"sooperset/mcp-atlassian@b041733"},"references":[{"type":"WEB","url":"https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/77xxx/CVE-2026-77268.json"},{"type":"ADVISORY","url":"https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-4596-2p6p-28cv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-77268"},{"type":"FIX","url":"https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460"},{"type":"FIX","url":"https://github.com/sooperset/mcp-atlassian/pull/1448"},{"type":"PACKAGE","url":"https://github.com/sooperset/mcp-atlassian"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:59:48.008403136Z"}}