Back to skill

Security audit

Wahoo Fitness Cloud API

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it says, but it handles OAuth tokens and detailed workout/GPS data with several security weaknesses users should review before installing.

Review this before installing if you store Wahoo data on shared machines, synced folders, or agent workspaces. Use a private WAHOO_BASE_DIR with restrictive permissions, avoid logging OAuth setup output, consider reducing OAuth scopes, and fix the dependency name/pinning before running pip install. The observed risks are under-scoped handling of sensitive fitness data and credentials, not evidence that the skill is intentionally harmful.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (6)

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding

Unpinned and inconsistent FIT parser dependency

Content
View full analysis
=1.2,<2'" ) from e return fitparse ``` ### Technical Analysis `requirements.txt` installs the unversioned package `fit-parser`, while the code imports `fitparse` and explicitly instructs users to install `fitparse>=1.2,<2`. Package names `fit-parser` and `fitparse` are distinct normalized distribution names. This inconsistency can install an unintended package and does not guarantee that the required `fitparse` module is present. In addition, the dependency is not version-pinned and has no integrity hash, so future installation behavior can change without changes to the audited repository. Python packages may execute build or installation code during installation. Consequently, selecting an unintended, compromised, or malicious distribution could lead to code execution with the privileges of the user running `pip install -r requirements.txt`. ### Attack Path 1. A user follows the documented installation command: `pip install -r requirements.txt`. 2. The package index resolves the unversioned `fit-parser` distribution. 3. A compromised or malicious release executes package installation code. 4. The package gains the installing user's privileges and can access that user's files and environment. 5. The expected `fitparse` import may still fail, obscuring the fact that an unrelated dependency was installed. ### Impact Assessment Successful supply-chain exploitation could execute arbitrary code as the install ...[truncated 257 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
lib/wahoo_auth.py:81
Finding

OAuth authorization flow lacks state binding and PKCE

Content
View full analysis
str: client_id, _, redirect_uri, scopes = _client_credentials() qs = urllib.parse.urlencode( { "client_id": client_id, "redirect_uri": redirect_uri, "scope": scopes, "response_type": "code", } ) return f"{OAUTH_AUTHORIZE_URL}?{qs}" ``` The setup script accepts any pasted URL or raw code without binding it to the authorization request initiated by this process: ```python raw = input("Paste the redirect URL (or just the code): ") code = _extract_code(raw) print() print(f"Exchanging code ({code[:8]}…) for tokens…") try: payload = wahoo_auth.exchange_code(code) except wahoo_auth.WahooAuthError as e: raise SystemExit(f"❌ {e}") ``` ### Technical Analysis OAuth authorization requests should include an unpredictable `state` value and validate the exact returned value before exchanging a code. Public or native clients should additionally use PKCE. The current implementation does neither. The manual paste workflow therefore cannot establish that the supplied code resulted from the authorization URL generated by the current setup process. A user can be induced to paste an authorization code or redirect URL originating from another OAuth session. Because the token endpoint still validates the client, redirect URI, and code, this flaw does not automatically let an attacker obtain the victim's tokens. Its principal effect is login/account substitution: the local installation may become connected to an account selected by an attacker, resulting in incorrect synchronization and persistent data contamination. ### Attack Path 1. An attacker init ...[truncated 925 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/oauth_setup.py:59
Finding

OAuth token and authorization-code prefixes are written to terminal output

Content
View full analysis
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Note
Location
lib/wahoo_auth.py:21
Finding

Excessive default OAuth scope requests user profile access

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_workouts.py:33
Finding

Sensitive workout, GPS, and device data is created without restrictive file modes

Content
View full analysis
sqlite3.Connection: TRAINING_DIR.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) with open(SCHEMA_PATH) as f: conn.executescript(f.read()) ``` FIT files are likewise opened without an explicit restrictive mode: ```python def download_fit(url: str, dest: Path) -> Path: """Download a FIT file from Wahoo's CDN. No auth required.""" dest.parent.mkdir(parents=True, exist_ok=True) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=120) as resp, open(dest, "wb") as f: while True: chunk = resp.read(64 * 1024) if not chunk: break f.write(chunk) return dest ``` ### Technical Analysis The SQLite database stores precise GPS coordinates, timestamps, heart rate, power, device serial numbers, workout tokens, and FIT download URLs. Raw FIT files contain equivalent high-resolution location and health data. Unlike the OAuth token file, which is eventually changed to mode `0600`, the database, FIT files, and their directories are never assigned restrictive permissions. Their effective modes depend on the process umask. With a permissive or common multi-user umask, files may be group-readable or world-readable, and directories may permit traversal. The risk is particularly significant when `WAHOO_BASE_DIR` points to a shared directo ...[truncated 982 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_workouts.py:532
Finding

Unvalidated external FIT URL permits arbitrary outbound requests and unbounded downloads

Content
View full analysis
Path: """Download a FIT file from Wahoo's CDN. No auth required.""" dest.parent.mkdir(parents=True, exist_ok=True) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=120) as resp, open(dest, "wb") as f: while True: chunk = resp.read(64 * 1024) if not chunk: break f.write(chunk) return dest ``` ### Technical Analysis `fit_url` crosses a network trust boundary but is not validated for scheme, hostname, port, resolved address, content type, or response size. `urllib.request.urlopen` can follow HTTP redirects and supports schemes beyond HTTPS. If an attacker can influence workout detail data, a compromised API/CDN response, or a redirect destination, the skill can be induced to request loopback or private-network resources. The response is saved locally and passed to a parser. Independently, the absence of a maximum byte count allows a large or endless response to consume disk space. The socket timeout i ...[truncated 1260 chars]
Remediation
View remediation
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
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (17)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description presents a broader/cloud-facing integration: connecting to the Wahoo Fitness Cloud API, retrieving workouts, downloading FIT files, and then analyzing them. The actual code chunk only implements the analysis/parsing portion for a local .fit file path. It lazy-imports fitparse, opens a local FIT file, and extracts metrics including power, HR, cadence, GPS, laps, devices, and zones. There is no network code, authentication, API client logic, workout listing, or download behavior. Because key declared capabilities and resource access patterns are absent, the description does not accurately represent this code chunk.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

The declared description emphasizes cloud API access to Wahoo Fitness, fetching workouts, and downloading FIT files before analysis. The supplied code chunk instead only implements a local command-line FIT parsing utility. While parsing FIT data is consistent with the analysis portion of the description, the primary behaviors present here are local file parsing and JSON output, not remote API interaction or workout retrieval. This is a material description-behavior mismatch for this code chunk.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
"""Wahoo Cloud API client.

Thin wrapper over /v1/workouts and /v1/user. Auto-refreshes the access
token on 401 (once), and respects rate-limit headers — backs off on 429
using X-RateLimit-Reset.
"""

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · lib/wahoo_api.py (reported line 3)May include surrounding context.

python
"""Wahoo Cloud API client.

Thin wrapper over /v1/workouts and /v1/user. Auto-refreshes the access
token on 401 (once), and respects rate-limit headers — backs off on 429
using X-RateLimit-Reset.
"""

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · lib/wahoo_auth.py (reported line 5)May include surrounding context.

python
"""Wahoo Cloud API client.

Thin wrapper over /v1/workouts and /v1/user. Auto-refreshes the access
token on 401 (once), and respects rate-limit headers — backs off on 429
using X-RateLimit-Reset.
"""

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · lib/wahoo_auth.py (reported line 167)May include surrounding context.

python
"""Wahoo Cloud API client.

Thin wrapper over /v1/workouts and /v1/user. Auto-refreshes the access
token on 401 (once), and respects rate-limit headers — backs off on 429
using X-RateLimit-Reset.
"""

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill description states it synchronizes Wahoo fitness data into a local SQLite database, which can include sensitive health and location-related workout information. The README explains what is stored, but does not clearly warn users that detailed personal activity data will be downloaded and retained locally.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

The file specifies locations for OAuth tokens and client credentials, including a secrets directory and optional environment file, but does not include a caution that these files contain sensitive credentials. For a markdown skill description, this omits an important warning about protecting tokens and secrets from disclosure.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill advertises executable workflows that use environment variables, local file reads/writes, network access, and shell commands, but it declares no explicit tool scope or permissions boundary. This increases the chance an agent runtime will grant broader capabilities than intended, making secret exposure, unintended filesystem access, or unreviewed command execution more likely.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The CLI parses a FIT file and writes the full JSON result to standard output, including GPS coordinates, heart rate, power, cadence, device information, and timestamps gathered earlier in the file. While the code's purpose is to parse FIT data, there is no visible warning, confirmation, or privacy disclosure in the CLI help text or nearby comments that this output may expose sensitive health and location data.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · lib/wahoo_auth.py (reported line 18)May include surrounding context.

python
from pathlib import Path
from typing import Optional

OAUTH_AUTHORIZE_URL = "https://api.wahooligan.com/oauth/authorize"
OAUTH_TOKEN_URL = "https://api.wahooligan.com/oauth/token"

DEFAULT_SCOPES = "workouts_read offline_data user_read"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · lib/wahoo_auth.py (reported line 19)May include surrounding context.

python
from pathlib import Path
from typing import Optional

OAUTH_AUTHORIZE_URL = "https://api.wahooligan.com/oauth/authorize"
OAUTH_TOKEN_URL = "https://api.wahooligan.com/oauth/token"

DEFAULT_SCOPES = "workouts_read offline_data user_read"

Tainted flow: 'TOKENS_PATH' from os.environ.get (line 26, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
88% confidence
Finding

The token file path is derived from WAHOO_BASE_DIR, which in turn can be controlled by environment variables. In an agent or multi-user context, an attacker who can influence the environment could redirect token writes to an unintended location, causing credential disclosure, overwrite of sensitive files accessible to the process, or storage in an insecure directory.

Content

Scanner excerpt · lib/wahoo_auth.py (reported line 154)May include surrounding context.

python
record = dict(payload)
    if "expires_in" in record:
        record["expires_at"] = int(time.time()) + int(record["expires_in"])
    with open(TOKENS_PATH, "w") as f:
        json.dump(record, f, indent=2)
    os.chmod(TOKENS_PATH, 0o600)

Context Window Stuffing

Medium
Category
Memory Poisoning
Confidence
80% confidence
Finding

Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Content

Scanner excerpt · scripts/fetch_workouts.py (reported line 136)May include surrounding context.

python
ascent_m, cadence_avg, calories, heart_rate_avg, power_avg,
            power_np, power_tss, speed_avg_ms, work_j, time_zone,
            fit_url, synced
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        ON CONFLICT(id) DO UPDATE SET
            name=excluded.name,
            starts=excluded.starts,

Context Window Stuffing

Medium
Category
Memory Poisoning
Confidence
80% confidence
Finding

Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Content

Scanner excerpt · scripts/fetch_workouts.py (reported line 323)May include surrounding context.

python
lap_trigger,
                time_in_zone1, time_in_zone2, time_in_zone3,
                time_in_zone4, time_in_zone5, time_in_zone6
            ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
            """,
            (
                workout_id, i + 1,

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The download_fit function creates parent directories and writes downloaded content to the provided destination path, which is a file-system modifying operation. Although the function has a brief docstring about auth, this file does not include any user-facing disclosure, confirmation, or warning about the write action itself.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
94% confidence
Finding

The dependency is unpinned, so installs will resolve to whatever version is current at install time. That creates a supply-chain risk: a malicious or compromised upstream release, or an unexpected breaking change, could alter behavior or introduce vulnerabilities without any change to this repository. Because this skill parses FIT files from a cloud-connected fitness workflow, dependency integrity matters and arbitrary version drift is not desirable.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
fit-parser

Static analysis

No suspicious patterns detected.