Back to skill

Security audit

Garmin Sync Cn To Global

Security checks for vulnerabilities and agentic risk

Overview

The skill appears to perform the Garmin sync it claims, but it needs Review because it stores Garmin account passwords in plaintext and uses weak local file handling.

Install only if you are comfortable giving this code Garmin CN and Global credentials and storing those passwords locally in plaintext. Use a dedicated Garmin password if possible, run it in a private user account or virtual environment, restrict ~/.config/garmin-sync permissions, avoid shell history exposure for passwords, and rotate/remove credentials if you stop using it.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
sync.py:52
Finding
Garmin Account Passwords Are Accepted Through Command-Line Arguments and Stored in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `sync.py:52-71`, `sync.py:255-259` **Vulnerability Type**: Plaintext credential storage and command-line secret exposure **Risk Level**: High ### Vulnerable Code ```python def save_credentials(email_cn, password_cn, email_global=None, password_global=None): """Save credentials - supports same or different credentials for CN and Global""" os.makedirs(CONFIG_DIR, exist_ok=True) creds = { 'email_cn': email_cn, 'password_cn': password_cn, } # If different credentials for Global if email_global and password_global: creds['email_global'] = email_global creds['password_global'] = password_global else: # Use same credentials creds['email_global'] = email_cn creds['password_global'] = password_cn with open(CONFIG_FILE, 'w') as f: json.dump(creds, f) os.chmod(CONFIG_FILE, 0o600) ``` ```python cred_parser = subparsers.add_parser('set-credentials', help='Set credentials') cred_parser.add_argument('--email-cn', required=True, help='Garmin China email') cred_parser.add_argument('--password-cn', required=True, help='Garmin China password') cred_parser.add_argument('--email-global', help='Garmin Global email (optional, defaults to CN)') cred_parser.add_argument('--password-global', help='Garmin Global password (optional, defaults to CN)') ``` The plaintext storage is also explicitly documented in `SKILL.md:41-42`. ### Technical Analysis The application serializes Garmin CN and Global passwords directly into `~/.config/garmin-sync/credentials.json` without encryption or integration with an operating-system credential store. File mode `0600` limits ordinary cross-user reads, but it does not protect credentials from processes executing as the same user, exposed backups, accidental copies, or compromise of the user account. Permissions are changed only after the file has been opened, written, and closed. Consequently, ...[truncated 1848 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Store credentials in an operating-system-backed credential manager such as Keychain, Secret Service, or Windows Credential Manager. - Prefer protected Garmin session tokens over retaining account passwords when the authentication library supports this. - Obtain passwords interactively with `getpass.getpass()` instead of command-line options. - If noninteractive operation is required, accept secrets through a protected file descriptor or credential-store reference rather than environment variables or process arguments. - Create any unavoidable secret file atomically with mode `0600`, using exclusive creation and symbolic-link protections. - Create `~/.config/garmin-sync` with mode `0700`. - Avoid printing passwords or including them in exception messages. - Document how users can rotate credentials and securely remove existing plaintext credential files. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
sync.py:96
Finding
Predictable Temporary Archive Path Allows Symbolic-Link File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `sync.py:96-108` **Vulnerability Type**: Insecure predictable temporary file **Risk Level**: Medium ### Vulnerable Code ```python # Download from CN (returns zip file) fit_url = f"/download-service/files/activity/{act_id}" fit_zip = client_cn.download(fit_url) # Save zip to temp file zip_path = f'/tmp/garmin_sync_{act_id}.zip' with open(zip_path, 'wb') as f: f.write(fit_zip) # Extract with zipfile.ZipFile(zip_path, 'r') as z: fit_filename = [n for n in z.namelist() if n.endswith('.fit')][0] fit_data = z.read(fit_filename) os.remove(zip_path) ``` ### Technical Analysis The application constructs a temporary filename in the shared `/tmp` directory from a predictable activity identifier. It then opens that path with ordinary write mode, which follows symbolic links and does not require exclusive file creation. A local attacker who can predict or discover an activity ID can place a symbolic link at the expected path before synchronization. When the victim runs the script, Python follows the link and truncates or overwrites the linked target with the downloaded ZIP data. The target must be writable by the victim account. The archive does not need to be persisted to disk because `zipfile.ZipFile` supports in-memory byte streams. The temporary file therefore grants unnecessary filesystem access for the declared synchronization behavior. ### Attack Path 1. A local attacker predicts an activity identifier that the victim will synchronize. 2. The attacker creates `/tmp/garmin_sync_<activityId>.zip` as a symbolic link to a file writable by the victim. 3. The victim runs the synchronization command. 4. `open(zip_path, 'wb')` follows the symbolic link and truncates or overwrites the target with Garmin archive data. 5. The subsequent ZIP operation may fail, but the target file has already been corrupted. 6. Cleanup may also fail under sticky-directory rules, but that does not reverse the overwrite. ...[truncated 304 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Avoid creating a temporary file and process the downloaded archive in memory: ```python with zipfile.ZipFile(io.BytesIO(fit_zip), 'r') as z: fit_files = [name for name in z.namelist() if name.endswith('.fit')] if len(fit_files) != 1: raise ValueError("Archive must contain exactly one FIT file") fit_data = z.read(fit_files[0]) ``` If disk-backed storage is unavoidable: - Use `tempfile.NamedTemporaryFile()` or `tempfile.TemporaryDirectory()`. - Require secure exclusive creation. - Do not derive the filename solely from attacker-influenced or predictable data. - Ensure cleanup occurs in a `finally` block. - Reject symbolic links and avoid shared-directory race conditions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
sync.py:29
Finding
Synchronization State and Failed Activity Metadata Are Written Without Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `sync.py:29-37`, with writes at `sync.py:167`, `sync.py:226`, and `sync.py:238-240` **Vulnerability Type**: Insecure storage permissions for sensitive activity metadata **Risk Level**: Medium ### Vulnerable Code ```python def load_json(path, default=None): if os.path.exists(path): with open(path, 'r') as f: return json.load(f) return default def save_json(path, data): os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w') as f: json.dump(data, f, indent=2) ``` Failed activity objects and synchronization state are subsequently persisted: ```python # Save updated failed records save_json(FAILED_FILE, failed_records) ``` ```python # Track failed for next retry key = (date, a.get('distance')) failed_records[str(key)] = a failed += 1 ``` ```python # Update state state['last_sync'] = time.strftime('%Y-%m-%d %H:%M:%S') state['last_activity_time'] = latest_time save_json(STATE_FILE, state) ``` ### Technical Analysis Unlike the credential file, `sync_state.json` and `failed_records.json` are written through a generic helper that does not set restrictive permissions. Their effective modes depend on the process umask. Under a permissive umask, other local users may be able to read these files. `failed_records.json` stores complete activity objects returned by the Garmin API rather than only the minimum fields required for a retry. Such objects can contain activity identifiers, local timestamps, distances, and other fitness metadata. This information may reveal routines or location-adjacent patterns. The same helper opens existing paths normally and therefore also lacks atomic replacement and symbolic-link protections. ### Attack Path 1. The script encounters a failed activity upload or updates synchronization state. 2. `save_json()` creates the corresponding JSON file using permissions derived from the current umask. 3. On a system with permissive ...[truncated 550 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create `~/.config/garmin-sync` with mode `0700`. - Create all state files atomically with mode `0600`, independent of the user's umask. - Write to a securely created temporary file in the same directory, flush it, and atomically replace the destination. - Reject symbolic links and unexpected file types before reading or replacing files. - Store only the minimum fields needed to retry an activity, such as its trusted identifier and essential comparison metadata. - Apply restrictive permissions to existing installations through a migration step. - Consider integrity validation and schema checking before consuming persisted state. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:7
Finding
Unpinned Third-Party Authentication Dependency Creates Supply-Chain Exposure<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:7-8` and `SKILL.md:23-25` **Vulnerability Type**: Unpinned security-sensitive dependency **Risk Level**: Medium ### Vulnerable Code ```bash # Install dependencies pip install garth ``` ```markdown ## Requirements - Python 3.x - garth library (`pip install garth`) ``` ### Technical Analysis The installation instructions request the latest package named `garth` without a reviewed version constraint, lock file, or integrity hash. The effective code installed can therefore change after the Skill itself has been audited. This dependency is security-sensitive because the script passes Garmin passwords to its authentication functionality and relies on it for authenticated communication with `garmin.cn` and `garmin.com`. A compromised package release or transitive dependency could access credentials, tokens, activity data, and the privileges of the user running the installation or script. No evidence was found that the currently intended `garth` package is malicious. The finding concerns reproducibility and supply-chain hardening. ### Attack Path 1. A user follows the documentation and runs `pip install garth`. 2. Package resolution selects the latest available release and its current transitive dependencies. 3. If a package-index account, release artifact, dependency, or configured index has been compromised, malicious code is installed. 4. Installation hooks or imported package code executes with the user's privileges. 5. Because the script supplies Garmin credentials to the dependency, malicious code can capture credentials or authenticated tokens and access local files available to the user. ### Impact Assessment A malicious dependency could execute arbitrary code with the invoking user's privileges, access Garmin credentials and activity records, and read or modify user-accessible files. The exact impact depends on the package source, execution environment, and privileges used during instal ...[truncated 95 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `garth` to a specific reviewed version. - Provide a lock file containing all transitive dependency versions. - Use cryptographic hashes, such as pip's `--require-hashes`, for reproducible installation. - State the expected trusted package index explicitly and avoid untrusted extra indexes. - Install dependencies inside an isolated virtual environment as a nonprivileged user. - Review release changes before updating pinned versions. - Use automated dependency vulnerability and provenance checks in the release process. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (8)

Credential Access

High
Category
Privilege Escalation
Content
# Install dependencies
pip install garth

# Set credentials (once, stored in ~/.config/garmin-sync/credentials.json)
garmin-sync set-credentials --email your_email --password your_password

# Sync new activities from CN to Global
Confidence
98% confidence
Finding
The skill instructs users to provide account credentials and states they will be stored in `~/.config/garmin-sync/credentials.json`, which implies persistent local credential storage. Combined with the later explicit note that storage is in plaintext, this creates a significant risk of credential theft from local compromise, backups, logs, or other processes with file access.

Credential Access

High
Category
Privilege Escalation
Content
## Security Considerations

- Credentials are stored in plaintext at `~/.config/garmin-sync/credentials.json`
- Set restrictive file permissions after first run: `chmod 600 ~/.config/garmin-sync/credentials.json`
- Consider using a dedicated/sandbox account for testing
- Review the code before running with your primary credentials
Confidence
99% confidence
Finding
The documentation explicitly acknowledges that credentials are stored in plaintext. Plaintext secret storage materially increases the chance that Garmin account credentials are exposed through local malware, shared systems, filesystem backups, accidental disclosure, or weak host permissions.

Credential Access

High
Category
Privilege Escalation
Content
## Security Considerations

- Credentials are stored in plaintext at `~/.config/garmin-sync/credentials.json`
- Set restrictive file permissions after first run: `chmod 600 ~/.config/garmin-sync/credentials.json`
- Consider using a dedicated/sandbox account for testing
- Review the code before running with your primary credentials
Confidence
97% confidence
Finding
Although `chmod 600` reduces access for other local users, this line reinforces a design that depends on a plaintext credential file. Hardening permissions helps but does not mitigate exposure to the account owner context, malware, backups, or any process already running as that user, so the underlying credential-handling weakness remains.

Credential Access

High
Category
Privilege Escalation
Content
from garth.sso import login

CONFIG_DIR = os.path.expanduser('~/.config/garmin-sync')
CONFIG_FILE = os.path.join(CONFIG_DIR, 'credentials.json')
STATE_FILE = os.path.join(CONFIG_DIR, 'sync_state.json')
FAILED_FILE = os.path.join(CONFIG_DIR, 'failed_records.json')
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
## Security Considerations

- Credentials are stored in plaintext at `~/.config/garmin-sync/credentials.json`
- Set restrictive file permissions after first run: `chmod 600 ~/.config/garmin-sync/credentials.json`
- Consider using a dedicated/sandbox account for testing
- Review the code before running with your primary credentials
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script stores Garmin account passwords in plaintext JSON on disk, which exposes long-lived credentials to any local process, backup system, or user that can read the file. Although the file is chmodded to 0600, that control is not sufficient against local compromise, credential theft from backups, or accidental disclosure, and the CLI provides no warning before persisting secrets.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The line explicitly states that all output is in English for consistency, which imposes a language choice on users. Under the policy rule, language constraints should either be optional for the user or clearly justified as region-specific or compliance-driven.

Natural-Language Policy Violations

Low
Confidence
98% confidence
Finding
The comment and behavior indicate a fixed language policy: output is always in English. This is a natural-language locale choice imposed by the skill with no user option to select another language or opt in.

Static analysis

No suspicious patterns detected.