T09 · Insecure Skill Coding Practices
- Location
garmin_health.py:58- Finding
Sensitive health records are stored without restrictive filesystem permissions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This Garmin skill appears to do what it claims, but it handles account credentials and persists sensitive health data locally with safeguards that users should review before installing.
Install only if you are comfortable giving the skill access to your Garmin account and storing detailed health data on this machine. Prefer macOS Keychain over CLI passwords or ~/.garmin_credentials, keep the data and token directories private, avoid shared/synced locations for GARMIN_DATA_DIR and GARMIN_TOKENSTORE, and consider pinning the garminconnect dependency before use.
garmin_health.py:58Sensitive health records are stored without restrictive filesystem permissions
garmin_health.py:263Plaintext credential file is used despite unsafe permissions
garmin_health.py:789Garmin account passwords can be supplied through exposed command-line arguments
skill.json:53Security-sensitive third-party dependency is not pinned to a reviewed version
The documented authentication flow includes reading credentials from multiple local sources, including Keychain and a plaintext file. Credential access is expected for this skill’s purpose, but handling account secrets across several storage channels materially increases the chance of exposure if the host is misconfigured or the skill is run in an untrusted environment.
Auth priority:
1. --email / --password CLI args
2. GARMIN_EMAIL / GARMIN_PASSWORD environment variables
3. macOS Keychain: security find-generic-password -s 'garmin_connect'
4. ~/.garmin_credentials (format: email=..., password=...)
Default data location:
This function actively loads Garmin credentials from environment variables, Keychain, and a local file. In a health-data skill, account compromise can expose not just login access but a broad history of sensitive wellness and activity information, so the credential-access surface is security-relevant even if functionally necessary.
# ── Garmin Client ──────────────────────────────────────────────────────────
def _load_credentials(email_arg=None, password_arg=None):
"""Load credentials from args -> env -> macOS keychain -> file."""
email = email_arg or os.environ.get("GARMIN_EMAIL")
password = password_arg or os.environ.get("GARMIN_PASSWORD")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"env": [
{
"name": "GARMIN_EMAIL",
"description": "Your Garmin Connect account email address. Can also be set via macOS Keychain or ~/.garmin_credentials file.",
"required": false
},
{
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"env": [
{
"name": "GARMIN_EMAIL",
"description": "Your Garmin Connect account email address. Can also be set via macOS Keychain or ~/.garmin_credentials file.",
"required": false
},
{
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"env": [
{
"name": "GARMIN_EMAIL",
"description": "Your Garmin Connect account email address. Can also be set via macOS Keychain or ~/.garmin_credentials file.",
"required": false
},
{
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"env": [
{
"name": "GARMIN_EMAIL",
"description": "Your Garmin Connect account email address. Can also be set via macOS Keychain or ~/.garmin_credentials file.",
"required": false
},
{
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
- ✅ **仅与 Garmin 官方 API 通信** -- 无额外网络请求
- ✅ **支持 macOS 钥匙串** -- 密码可加密存储,无需明文
- ⚠️ **避免 --password 参数** -- 命令行密码会留在 shell 历史和进程列表中,建议用环境变量或钥匙串
- ⚠️ **保护配置文件** -- 使用 `~/.garmin_credentials` 时务必 `chmod 600`
## 常见问题
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
- ✅ **仅与 Garmin 官方 API 通信** -- 无额外网络请求
- ✅ **支持 macOS 钥匙串** -- 密码可加密存储,无需明文
- ⚠️ **避免 --password 参数** -- 命令行密码会留在 shell 历史和进程列表中,建议用环境变量或钥匙串
- ⚠️ **保护配置文件** -- 使用 `~/.garmin_credentials` 时务必 `chmod 600`
## 常见问题
The skill documentation describes capabilities involving environment variables, shell commands, and local file writes, but it does not declare any explicit tool scope or permission boundaries. This can cause an agent platform to invoke the skill with broader-than-expected access, increasing the risk of credential exposure or unauthorized filesystem changes if the skill is auto-enabled.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
Option D -- Credentials file:
echo -e "email=you@example.com\npassword=yourpassword" > ~/.garmin_credentials
chmod 600 ~/.garmin_credentials
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
Option D -- Credentials file:
echo -e "email=you@example.com\npassword=yourpassword" > ~/.garmin_credentials
chmod 600 ~/.garmin_credentials
The trigger phrases are broad, natural-language health questions that could easily match ordinary user conversation and cause the skill to run unexpectedly. In this context, unintended activation is more sensitive because the skill accesses private health data and may prompt for or use stored credentials.
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS daily_summary (
date TEXT PRIMARY KEY,
fetched_at TEXT,
steps INTEGER,
The skill reads Garmin credentials from CLI args, environment variables, Keychain, and a plaintext dotfile, but the handling and risks are only partially disclosed. Especially problematic is support for ~/.garmin_credentials, which encourages plaintext secret storage and increases the chance of credential leakage through filesystem exposure, backups, or misconfigured permissions.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
if not password:
try:
kc = subprocess.run(
["security", "find-generic-password", "-s", "garmin_connect", "-w"],
capture_output=True,
text=True,
Beyond high-level health metrics, the skill fetches and stores detailed workout structure such as exercise sets, laps, and performance summaries. That expands collection of sensitive personal data beyond the plainly stated purpose, increasing privacy risk and the blast radius if local storage is accessed by others.
The skill persistently stores highly sensitive health and fitness data to JSON and SQLite on disk, but it provides no clear runtime consent, retention policy, or file-permission hardening. On multi-user systems, shared backups, synced home directories, or compromised endpoints, this can expose intimate medical and behavioral data well beyond the immediate execution of the skill.
The JSON output path is derived from DATA_DIR, which can be controlled through the GARMIN_DATA_DIR environment variable, and the code writes to that path without validation. In contexts where untrusted environment variables can be injected or the skill runs with elevated privileges, this can redirect sensitive health data writes to unintended files or locations, causing data exposure or file clobbering.
p(f" 本周运动: 中强度 {result.get('weekly_moderate_minutes')}min 高强度 {result.get('weekly_vigorous_minutes')}min / 目标 {result.get('weekly_intensity_goal')}min")
# ── 保存 JSON ──────────────────────────────
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if target_date == str(date.today()):
latest_file is built from DATA_DIR, which comes from an environment variable, and is written without any path safety checks. If an attacker can influence the process environment or filesystem layout, they may cause overwriting of arbitrary writable files or redirect sensitive health data into an attacker-observable location.
if target_date == str(date.today()):
latest_file = os.path.join(DATA_DIR, "latest.json")
with open(latest_file, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
# ── 写入 SQLite daily_summary ──────────────
The manifest explicitly states that sensitive health snapshots and OAuth tokens are stored locally, but it does not clearly warn users about the privacy and security implications of that persistence. Health telemetry and authentication tokens are both sensitive; if written to default home-directory locations without strong permissions, encryption, or disclosure, they may be exposed to other local users, backups, or malware.
The line explicitly describes the skill as having an "English UI," which can be a language/locale policy concern when no user choice, opt-in, or justification is provided. Under the policy rules, forcing a specific language without opt-in is reportable across all file types, including changelog text.
The documentation states that all labels and output are in English, but does not indicate that this is optional or user-configurable. This is a natural-language policy concern because it imposes a specific language without explicit user opt-in or a documented necessity.
Many user-facing strings, status messages, and warnings are hardcoded in Chinese, while the script description and argument help are partly in English. This imposes a locale/language choice on users without an explicit opt-in or documented region-specific justification.
The skill reaches outside normal API use and invokes the macOS Keychain through a system utility, but this capability is not disclosed in the stated purpose. While not inherently malicious, undisclosed credential-store access weakens informed consent and may surprise users reviewing the skill’s behavior.
No suspicious patterns detected.