Back to skill

Security audit

Fitbit Health Skill

Security checks for vulnerabilities and agentic risk

Overview

This Fitbit skill appears purpose-built rather than malicious, but it deserves Review because it handles sensitive health data, stores OAuth tokens, and has scope and token-storage weaknesses.

Install only if you are comfortable letting the skill and your agent access Fitbit health/profile data. Review the OAuth consent screen carefully, avoid sharing verbose or JSON outputs, and consider hardening token storage and pinning dependencies before using it in a multi-user or higher-risk environment.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
src/storage/tokens.ts:29
Finding

OAuth Tokens Written Through a Predictable and Initially Unprotected Temporary File

Content
View full analysis

Vulnerability Details

File Location: src/storage/tokens.ts, lines 29-34
Vulnerability Type: Insecure temporary file handling and sensitive token exposure
Risk Level: Medium

Vulnerable Code

ts
export async function writeTokens(tokens: TokenData): Promise<void> {
  await ensureTokensDir();
  const tempPath = `${TOKENS_PATH}.tmp`;
  await fs.writeFile(tempPath, JSON.stringify(tokens, null, 2), "utf8");
  await fs.rename(tempPath, TOKENS_PATH);
  await fs.chmod(TOKENS_PATH, 0o600);
}

Technical Analysis

OAuth access and refresh tokens are first written to the predictable path tokens.json.tmp. The temporary file is created without an explicit restrictive mode and without exclusive creation. Its initial permissions consequently depend on the process umask and may permit other local users to read it.

The final file is changed to mode 0600 only after the temporary file has been renamed. This creates a time-of-check/time-of-use exposure window. If the process terminates after the write or rename but before chmod, sensitive tokens can remain stored with permissive permissions.

Because the temporary pathname is fixed and writeFile does not use the wx flag, a pre-existing file is overwritten. If an attacker can write to the configuration directory, the attacker may create a symbolic link at tokens.json.tmp, causing the CLI to write token data to another user-writable target. Concurrent CLI processes can also interfere with one another through the shared temporary pathname.

The containing directory is created at src/storage/tokens.ts:13-15 without an explicit 0700 mode, further relying on environmental defaults:

ts
export async function ensureTokensDir(): Promise<void> {
  await fs.mkdir(CONFIG_DIR, { recursive: true });
}

Attack Path

  1. A local attacker identifies the predictable ~/.config/fitbit-cli/tokens.json.tmp path.
  2. The victim runs ` ...[truncated 1328 chars]
Remediation
View remediation

Remediation Suggestions

  1. Create and enforce the configuration directory with mode 0700, and verify that it is a real directory rather than a symbolic link.
  2. Generate a cryptographically random temporary filename in the same directory as the destination.
  3. Open the temporary file using exclusive creation (flag: "wx") and mode 0600.
  4. Write and flush the token data before atomically renaming the temporary file to tokens.json.
  5. Ensure the final file has mode 0600 before making it available at the final pathname.
  6. Remove the temporary file on every failure path using a try/finally block.
  7. Reject unexpected symbolic links and non-regular files when reading or replacing token storage.
  8. Serialize token updates or use unique temporary files to prevent concurrent processes from racing.

Example hardened creation options:

ts
await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });

const tempPath = path.join(
  CONFIG_DIR,
  `.tokens-${process.pid}-${crypto.randomBytes(16).toString("hex")}.tmp`,
);

try {
  await fs.writeFile(
    tempPath,
    JSON.stringify(tokens, null, 2),
    { encoding: "utf8", mode: 0o600, flag: "wx" },
  );
  await fs.rename(tempPath, TOKENS_PATH);
  await fs.chmod(TOKENS_PATH, 0o600);
} finally {
  await fs.unlink(tempPath).catch(() => undefined);
}
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (20)

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

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.

Content

Scanner excerpt · README.md (reported line 46)May include surrounding context.

md
## Setup

1. **Create a Fitbit App**
   - Go to [dev.fitbit.com/apps](https://dev.fitbit.com/apps)
   - Register a new app with:
     - **OAuth 2.0 Application Type:** Personal

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill invokes a CLI that performs OAuth and API queries, which implies both shell execution and network access, but it does not declare any explicit tool scope or permissions boundaries. That omission weakens reviewability and can let an agent use sensitive capabilities without clear policy constraints, which is especially risky for a skill handling personal health data.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill processes highly sensitive health information, yet it provides no warning that outputs may expose private activity, sleep, heart-rate, weight, or profile data. It also advertises a --verbose mode with debug/HTTP details, which can increase leakage of tokens, headers, or other sensitive request metadata if used carelessly.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 61)May include surrounding context.

md
- Dates default to today if omitted
- Date format: `YYYY-MM-DD` or `today`
- Tokens are stored in `~/.config/fitbit-cli/tokens.json` (chmod 600)
- Token refresh is automatic

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/client/api.ts (reported line 8)May include surrounding context.

ts
import { FitbitActivityResponse, FitbitProfileResponse } from "../types/fitbit.js";
import { ConfigData, TokenData } from "../types/config.js";

const API_BASE = "https://api.fitbit.com/1";

export class FitbitClient {
  constructor(

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/client/auth.ts (reported line 11)May include surrounding context.

ts
import { FitbitActivityResponse, FitbitProfileResponse } from "../types/fitbit.js";
import { ConfigData, TokenData } from "../types/config.js";

const API_BASE = "https://api.fitbit.com/1";

export class FitbitClient {
  constructor(

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/client/auth.ts (reported line 12)May include surrounding context.

ts
import { FitbitActivityResponse, FitbitProfileResponse } from "../types/fitbit.js";
import { ConfigData, TokenData } from "../types/config.js";

const API_BASE = "https://api.fitbit.com/1";

export class FitbitClient {
  constructor(

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
91% confidence
Finding

The dependency is specified with a caret range, which permits automatic installation of newer releases within the major version. For a security-sensitive agent skill that handles health-related data, this weakens supply-chain integrity because a future compromised or vulnerable minor/patch release could be pulled without explicit review.

Content

Scanner excerpt · package.json (reported line 45)May include surrounding context.

json
"SKILL.md"
  ],
  "dependencies": {
    "chalk": "^5.3.0",
    "cli-table3": "^0.6.5",
    "commander": "^12.0.0",
    "date-fns": "^3.6.0",

Unverifiable Dependency: chalk has 1 known advisory(ies) (MAL-2025-46969 (Malicious code in chalk (npm))), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
94% confidence
Finding

The manifest allows a version range for chalk while there is a known malicious-package advisory affecting some chalk releases. Because the version is not pinned, consumers cannot verify from this file alone whether installations will avoid the affected release, creating real supply-chain uncertainty.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
91% confidence
Finding

The dependency is specified with a caret range, which permits automatic installation of newer releases within the major version. For a CLI skill that may be installed in automated environments, this creates avoidable supply-chain exposure because builds are not fully reproducible and may consume an unsafe release later.

Content

Scanner excerpt · package.json (reported line 46)May include surrounding context.

json
],
  "dependencies": {
    "chalk": "^5.3.0",
    "cli-table3": "^0.6.5",
    "commander": "^12.0.0",
    "date-fns": "^3.6.0",
    "open": "^10.0.4"

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
91% confidence
Finding

Using a non-pinned semver range allows different installations to resolve to different commander releases over time. That increases the risk of accidental introduction of vulnerable or malicious upstream code, even if the current declared baseline is not itself known-bad.

Content

Scanner excerpt · package.json (reported line 47)May include surrounding context.

json
"dependencies": {
    "chalk": "^5.3.0",
    "cli-table3": "^0.6.5",
    "commander": "^12.0.0",
    "date-fns": "^3.6.0",
    "open": "^10.0.4"
  },

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
90% confidence
Finding

The date-fns package is not pinned to an exact version, so future installs may silently pull newer patch/minor releases. In an agent-integrated skill, reproducibility matters because upstream compromise or regression could affect deployed behavior without a source change in this repository.

Content

Scanner excerpt · package.json (reported line 48)May include surrounding context.

json
"chalk": "^5.3.0",
    "cli-table3": "^0.6.5",
    "commander": "^12.0.0",
    "date-fns": "^3.6.0",
    "open": "^10.0.4"
  },
  "devDependencies": {

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
95% confidence
Finding

The open package is unpinned, which is more concerning than a generic library because it interacts with the host environment to launch external resources. If an unsafe future release were resolved, it could expose users to command execution or browser-launch abuse on the machine running the skill.

Content

Scanner excerpt · package.json (reported line 49)May include surrounding context.

json
"cli-table3": "^0.6.5",
    "commander": "^12.0.0",
    "date-fns": "^3.6.0",
    "open": "^10.0.4"
  },
  "devDependencies": {
    "@types/node": "^20.11.30",

Unverifiable Dependency: open has 1 known advisory(ies) (GHSA-28xh-wpgr-7fm8 (Command Injection in open)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
96% confidence
Finding

The open package has a known command-injection advisory, and the manifest does not pin a specific safe version. In a CLI skill, especially one likely to run on a user's workstation, uncertainty around a host-interacting package materially increases risk because exploitation could lead to local command execution.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 52)May include surrounding context.

json
"open": "^10.0.4"
  },
  "devDependencies": {
    "@types/node": "^20.11.30",
    "esbuild": "^0.25.0",
    "tsx": "^4.7.1",
    "typescript": "^5.3.3"

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 53)May include surrounding context.

json
},
  "devDependencies": {
    "@types/node": "^20.11.30",
    "esbuild": "^0.25.0",
    "tsx": "^4.7.1",
    "typescript": "^5.3.3"
  }

Unverifiable Dependency: esbuild has 2 known advisory(ies) (GHSA-67mh-4wv8-2f99 (esbuild enables any website to send any requests to the development server and r); GHSA-g7r4-m6w7-qqqr (esbuild allows arbitrary file read when running the development server on Window)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 54)May include surrounding context.

json
"devDependencies": {
    "@types/node": "^20.11.30",
    "esbuild": "^0.25.0",
    "tsx": "^4.7.1",
    "typescript": "^5.3.3"
  }
}

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 55)May include surrounding context.

json
"@types/node": "^20.11.30",
    "esbuild": "^0.25.0",
    "tsx": "^4.7.1",
    "typescript": "^5.3.3"
  }
}

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

This code performs a network-backed retrieval of personal activity and health-related data via client.getActivity(resolvedDate). Although the command purpose implies activity retrieval, this file itself provides no explicit warning, notice, or comment that user health data will be accessed from an external service.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.