NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

CVE-2026-77268: MCP Atlassian: Insecure File Permissions on OAuth Token Storage

mediumCVSS 3.1 5.5GHSA-4596-2p6p-28cv

mcp-atlassian before 0.22.0 saves OAuth tokens to `~/.mcp-atlassian/oauth-` plus the client ID plus `.json` through `_save_tokens_to_file()` with default umask permissions, typically 0644 for the file and 0755 for the directory. Any local user can read the access and refresh tokens. Upgrade to 0.22.0.

Published Sep 22, 2026 · Analysis Oct 22, 2026

What an attacker can do

In utils/oauth.py, token_dir.mkdir(exist_ok=True) creates the directory with no mode, and open(token_path, "w") writes the file with no explicit mode. No os.chmod or os.fchmod follows. The file holds refresh_token, access_token, expires_at, cloud_id and base_url.

The keyring is the primary store, but the file copy is written in addition to it. When the keyring is unavailable, as in headless, container and CI environments, the file is the only store.

Another local user on the same machine reads the file, then calls the Atlassian API with the access token, for example /rest/api/3/myself under the stolen cloud_id. The refresh token can be exchanged at auth.atlassian.com/oauth/token for new access tokens, so access continues after the original token expires. The exchange also requires the OAuth client ID and secret, which the reporter's example takes from the environment. The stolen tokens carry the victim's Jira and Confluence permissions.

Shared servers, CI runners, multi-user workstations and containers with shared volumes are the exposed settings. The attacker needs a local account, as the CVSS vector reflects.

Who is affected

mcp-atlassian below 0.22.0 configured with OAuth. The risk is highest where the keyring is unavailable, and where other users can traverse the home directory.

pip show mcp-atlassian
ls -la ~/.mcp-atlassian/

A listing like -rw-r--r-- on oauth-*.json shows a world-readable token file.

mcp-atlassian

Ecosystem
PyPI
Affected
< 0.22.0
Fixed in
0.22.0
Check
pip show mcp-atlassian

How to fix it

Upgrade to 0.22.0:

pip install --upgrade mcp-atlassian==0.22.0

The reporter's recommended fix creates the directory with mode 0o700 and opens the file with os.open at 0o600, and tightens existing directories with os.chmod. Existing files created by older versions may keep their old modes, so run chmod 700 ~/.mcp-atlassian and chmod 600 ~/.mcp-atlassian/oauth-*.json yourself, then revoke and reissue the OAuth tokens if other users had access.

How to tell if you were hit

Check modes of ~/.mcp-atlassian and its oauth-*.json files on shared hosts, CI runners and container volumes. For other users' homes, an admin can run ls -la /home/*/.mcp-atlassian/. In the Atlassian admin console, look for OAuth token use from unfamiliar addresses or at unusual times. Auditd or file access logs that show another user reading the token file are direct evidence.

Original advisory

From the GitHub Advisory Database (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):

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

# 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

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

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:

# 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:

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

Credited to offset.

Questions

Is the keyring storage also insecure?

No. The advisory calls the keyring path secure. The problem is the plaintext file written alongside it, which is the only store when the keyring fails.

What can an attacker do with the refresh token?

Exchange it for new access tokens and keep Atlassian API access with the victim's permissions, until it is revoked or expires. The exchange also needs the OAuth client ID and secret.

Does upgrading fix files that already exist?

Not necessarily. Check and tighten the modes of existing files and directories yourself, and rotate the tokens.

Is this exploitable remotely?

No. The attacker needs a local account or filesystem access on the host.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries