Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

CVE-2026-54447 is a high-severity (CVSS 8.4) vulnerability in garminconnect. O3 Security confirms whether CVE-2026-54447 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

garminconnect Has Insecure Permission Assignment for Garmin OAuth Token Store

Also known asPYSEC-2026-3467
Published
Jul 15, 2026
Updated
Jul 23, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Jul 23, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐍garminconnect

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Insecure Permission Assignment for Garmin OAuth Token Store

Summary

garminconnect (≤ 0.3.4) wrote its OAuth token store to disk without restricting file-system permissions. Under the default Linux umask (022) the token file garmin_tokens.json was created world-readable (0o644). The file contains the DI refresh token, so any other local user on a shared host could read it and obtain persistent, unauthorized access to the victim's Garmin Connect account.

  • Severity: High
  • Weakness: CWE-732 (Incorrect Permission Assignment for Critical Resource)
  • Affected versions: <= 0.3.4
  • Patched version: 0.3.5

Details

Client.dump() created the token directory and file with no mode argument, leaving permissions entirely to the process umask:

def dump(self, path: str) -> None:
    p = Path(path).expanduser()
    if p.is_dir() or not p.name.endswith(".json"):
        p = p / "garmin_tokens.json"
    p.parent.mkdir(parents=True, exist_ok=True)   # no mode=
    p.write_text(self.dumps())                     # no permission restriction

The serialized payload includes di_token, di_refresh_token, and di_client_id. The call is in the core library (Garmin.login(tokenstore=...) persists tokens this way), and all shipped usage examples default the token store to ~/.garminconnect.

Under umask 022 the resulting permissions were:

  • token directory → 0o755
  • garmin_tokens.json0o644 (world-readable)

A separate, unprivileged user on the same machine could read the file with a plain open() — no elevated privileges required — and extract the refresh token.

Impact

Local credential theft / privilege escalation on multi-user Linux or macOS hosts running under a permissive umask. The stolen refresh token can be exchanged for fresh access tokens via Garmin's OAuth endpoint, granting ongoing access to the victim's account (health/fitness data, activity history, device management) until the token is revoked.

Patch

Fixed in 0.3.5 (commit 77a3837). dump() now creates the directory as 0o700 and writes the token file as 0o600 regardless of umask — using os.open(..., O_CREAT|O_WRONLY|O_TRUNC, 0o600) with O_NOFOLLOW where available, plus a defensive chmod that also tightens a pre-existing loose file:

p.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
with contextlib.suppress(OSError):
    p.parent.chmod(0o700)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
if hasattr(os, "O_NOFOLLOW"):
    flags |= os.O_NOFOLLOW
fd = os.open(p, flags, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
    f.write(self.dumps())
with contextlib.suppress(OSError):
    p.chmod(0o600)

Verified under umask 022: directory 0o700, file 0o600, no group/other access.

Workarounds

If you cannot upgrade immediately, restrict the token store manually and keep it owner-only:

chmod 700 ~/.garminconnect
chmod 600 ~/.garminconnect/garmin_tokens.json

Remediation

  1. Upgrade to garminconnect >= 0.3.5:
    pip install --upgrade garminconnect
    
  2. Fix any token file already on disk — upgrading only tightens permissions on the next write, so an existing world-readable file stays exposed until then:
    chmod 600 ~/.garminconnect/garmin_tokens.json
    # or remove it and log in again to mint a fresh token store
    
  3. If the file was exposed on a shared host, treat the refresh token as compromised. Re-authenticate (delete the token store and log in again) so a new token is issued; consider the previously stored token potentially read by others until rotated.

Credit

Reported by EQSTLab via a private security advisory. garminconnect thanks them for the detailed, responsible disclosure.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIgarminconnectall versions0.3.5

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for garminconnect. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update garminconnect to 0.3.5 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-54447 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether CVE-2026-54447 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to CVE-2026-54447. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Insecure Permission Assignment for Garmin OAuth Token Store ### Summary `garminconnect` (≤ 0.3.4) wrote its OAuth token store to disk without restricting file-system permissions. Under the default Linux umask (`022`) the token file `garmin_tokens.json` was created world-readable (`0o644`). The file contains the DI **refresh token**, so any other local user on a shared host could read it and obtain persistent, unauthorized access to the victim's Garmin Connect account. - **Severity:** High - **Weakness:** CWE-732 (Incorrect Permission Assignment for Critical Resource) - **Affected version
O3 Security · Impact-Aware SCA

Is CVE-2026-54447 in your dependencies?

O3 detects CVE-2026-54447 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

CVE-2026-54447: garminconnect Privilege… | O3 Security