Back to skill

Security audit

Wahoo Skill

Security checks for vulnerabilities and agentic risk

Overview

This Wahoo workout sync skill appears legitimate, but it needs review because it stores long-lived Wahoo tokens and sensitive GPS/health data locally while using broader access and less-bounded downloads than necessary.

Install only if you are comfortable giving this skill ongoing Wahoo account access and storing detailed workout, location, device, and health telemetry on this machine. Prefer removing user_read unless you need profile lookup, protect the Wahoo secrets and training directory with restrictive permissions, and be cautious running full syncs in shared or high-trust network environments until FIT URL validation and download size limits are added.

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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
lib/wahoo_api.py:118
Finding

Unrestricted FIT URL Retrieval Enables Server-Side Request Forgery and Resource Exhaustion

Content
View full analysis

Vulnerability Details

File Location: lib/wahoo_api.py:118-130; invoked from scripts/fetch_workouts.py:522-534
Vulnerability Type: Unvalidated remote URL retrieval
Risk Level: High

Vulnerable Code

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

The caller passes the URL directly from the API response:

python
summary = (detail.get("workout_summary") or {})
file_obj = summary.get("file") or {}
fit_url = file_obj.get("url")
if not fit_url:
    continue

dest = FIT_DIR / f"{wid}.fit"
if not dest.exists():
    try:
        wahoo_api.download_fit(fit_url, dest)
        fits_downloaded += 1
        print(f"  ⬇️   {wid}.fit ({dest.stat().st_size // 1024} KB)")
    except Exception as e:
        print(f"  ⚠️  download {wid}: {e}")
        continue

Technical Analysis

download_fit() accepts an arbitrary URL and passes it to urllib.request.urlopen() without validating:

  • The URL scheme
  • The destination hostname
  • Redirect destinations
  • The response content type
  • The response size
  • Whether the resolved address belongs to a private or loopback network

Although the current caller obtains the URL from an authenticated Wahoo API response, it crosses a remote trust boundary and should not be assumed safe. If that response, account data, CDN metadata, proxy traffic, or upstream service is compromised, the URL can point somewhere other than an approved Wahoo CDN.

urllib supports multiple URL handlers and follows H ...[truncated 1329 chars]

Remediation
View remediation

Remediation Suggestions

  • Parse URLs with urllib.parse.urlsplit() before opening them.
  • Permit only https URLs.
  • Maintain an explicit allowlist of documented Wahoo CDN hostnames.
  • Reject URLs containing user information, nonstandard ports, fragments, or malformed hostnames.
  • Resolve the hostname and reject loopback, link-local, private, multicast, and reserved addresses where appropriate.
  • Disable automatic redirects or validate every redirect target against the same policy.
  • Require an expected FIT content type or validate the downloaded FIT header before retaining the file.
  • Enforce a strict maximum download size using both Content-Length and a running byte counter.
  • Download into a restricted temporary file and atomically rename it only after successful validation.
  • Delete partial downloads after any error.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_workouts.py:32
Finding

Sensitive GPS and Health Data Are Stored Without Explicit Restrictive Permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/fetch_workouts.py:32-48, scripts/fetch_workouts.py:482-483; sensitive fields defined at schema/wahoo_db_schema.sql:128-145
Vulnerability Type: Insecure local storage permissions
Risk Level: Medium

Vulnerable Code

python
TRAINING_DIR = Path(
    os.environ.get(
        "WAHOO_TRAINING_DIR",
        os.path.expanduser("~/.openclaw/workspace/training"),
    )
)
DB_PATH = TRAINING_DIR / "wahoo.db"
FIT_DIR = TRAINING_DIR / "wahoo_fit"
SCHEMA_PATH = SKILL_ROOT / "schema" / "wahoo_db_schema.sql"


def init_db() -> 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())
    _run_migrations(conn)
    conn.commit()
    return conn
python
FIT_DIR.mkdir(parents=True, exist_ok=True)
conn = init_db()

The database stores sensitive telemetry:

sql
CREATE TABLE IF NOT EXISTS records (
    workout_id INTEGER NOT NULL,
    timestamp TEXT NOT NULL,
    power_w REAL,
    heart_rate REAL,
    cadence REAL,
    speed_ms REAL,
    enhanced_speed_ms REAL,
    distance_m REAL,
    altitude_m REAL,
    enhanced_altitude_m REAL,
    position_lat_deg REAL,
    position_long_deg REAL,
    grade REAL,
    temperature REAL,
    battery_soc REAL,
    gps_accuracy REAL,
    left_right_balance REAL,
    ascent_m REAL,
    descent_m REAL,
    calories REAL,
    FOREIGN KEY (workout_id) REFERENCES workouts(id)
);

Technical Analysis

The training directory and FIT directory are created without an explicit mode, while the SQLite database and downloaded FIT files are created under the process's current umask. Unlike the token file, which is explicitly changed to mode 0600, these highly sensitive artifacts receive no programmatic permission hardening.

The database and FIT ...[truncated 1279 chars]

Remediation
View remediation

Remediation Suggestions

  • Create the training and FIT directories with mode 0700.
  • Create the SQLite database and FIT files with mode 0600.
  • Explicitly apply restrictive permissions to existing artifacts after verifying that they are owned by the current user.
  • Reject symlinked training directories and files where feasible, or open files using safe no-follow semantics.
  • Use a restrictive temporary umask around artifact creation.
  • Document the sensitivity and retention period of GPS, health, and device data.
  • Provide an option to omit GPS coordinates, device serial numbers, raw FIT files, and workout_token.
  • Consider encrypted storage when the host or training directory is shared.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
lib/wahoo_auth.py:18
Finding

Default OAuth Scope Exceeds the Minimum Required for Workout Synchronization

Content
View full analysis

Vulnerability Details

File Location: lib/wahoo_auth.py:18-22; reflected in config.example.json:6-11 and SKILL.md:42-46
Vulnerability Type: Excessive OAuth authorization scope
Risk Level: Medium

Vulnerable Code

python
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"
DEFAULT_REDIRECT_URI = "https://localhost:8080/"

The example configuration applies the same default:

json
{
  "WAHOO_CLIENT_ID": "your-client-id-from-developers.wahooligan.com",
  "WAHOO_CLIENT_SECRET": "your-client-secret",
  "WAHOO_REDIRECT_URI": "https://localhost:8080/",
  "WAHOO_SCOPES": "workouts_read offline_data user_read"
}

Technical Analysis

Workout synchronization requires workouts_read, while automatic refresh requires offline_data. The synchronization pipeline does not call wahoo_api.get_user(), yet the default authorization request also asks for user_read.

According to the Skill documentation, the user endpoint can return first name, last name, email address, birth date, gender, height, and weight. Granting access to this profile information by default violates least-privilege principles because it is unrelated to the declared fetch-and-parse workflow.

The existence of an optional get_user() helper does not justify granting the scope to all users before that functionality is explicitly requested.

Attack Path

  1. A user follows the default setup instructions.
  2. The OAuth authorization request includes user_read.
  3. The user approves the combined scopes.
  4. The resulting access or refresh token grants profile access in addition to workout access.
  5. If the token file, environment, or Agent session is compromised, the attacker can call /v1/user and retrieve personal profile data that was not required for synchronization.

Impa

...[truncated 336 chars]

Remediation
View remediation

Remediation Suggestions

  • Change the default to workouts_read offline_data.
  • Request user_read only when the user explicitly invokes profile functionality.
  • Separate profile access into a distinct consent flow or clearly documented opt-in.
  • Display the exact scopes before opening the authorization URL.
  • Validate the returned scope and warn if the authorization server grants permissions beyond those requested.
  • Update README.md, SKILL.md, and config.example.json to describe the minimum synchronization scope separately from optional scopes.

T08 · Insecure Dependencies

Warning
Location
README.md:33
Finding

Third-Party Parser Dependency Is Installed Without Reproducible Version or Integrity Pinning

Content
View full analysis

Vulnerability Details

File Location: README.md:33-38; imported by lib/fit_parser.py:17-25
Vulnerability Type: Unverified third-party dependency installation
Risk Level: Medium

Vulnerable Code

bash
pip install --user 'fitparse>=1.2,<2'

The package is subsequently imported and executed:

python
def _ensure_fitparse():
    try:
        import fitparse  # type: ignore
    except ImportError as e:
        raise RuntimeError(
            "fitparse not installed. Run: pip install --user 'fitparse>=1.2,<2'"
        ) from e
    return fitparse

Technical Analysis

The version range permits any present or future fitparse release from version 1.2 up to, but excluding, version 2. No lockfile, exact version, package hash, signed artifact, or trusted index requirement is provided.

Python packages execute code during installation and import. If the configured package index is compromised, a matching release is malicious, or package resolution is redirected to an untrusted mirror, following the documented command can install attacker-controlled code. The broad version range also prevents reproducible review because different users may execute different dependency code.

This finding concerns unsafe dependency acquisition rather than evidence that the current fitparse package is malicious.

Attack Path

  1. An attacker publishes or substitutes a malicious fitparse artifact satisfying the documented version range.
  2. The user's pip configuration resolves the dependency from the affected package source.
  3. The user follows the README installation command.
  4. Malicious installation hooks may execute immediately.
  5. Alternatively, malicious module initialization executes when fit_parser.py imports fitparse.
  6. The code runs with the privileges of the user or Agent process and can access credentials, tokens, workout data, and writable workspace files.

Impa

...[truncated 290 chars]

Remediation
View remediation

Remediation Suggestions

  • Pin a reviewed exact dependency version.
  • Publish a lockfile or constraints file with cryptographic hashes.
  • Install with hash enforcement, such as pip install --require-hashes -r requirements.txt.
  • Document the expected trusted package index and avoid permissive extra indexes.
  • Regularly review and update the pinned dependency through a controlled process.
  • Consider vendoring a reviewed parser or using an isolated environment with minimal filesystem and network permissions.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/oauth_setup.py:59
Finding

OAuth Setup Discloses Reusable Token Prefixes to Terminal and Agent Logs

Content
View full analysis

Vulnerability Details

File Location: scripts/oauth_setup.py:59-70
Vulnerability Type: Partial credential disclosure
Risk Level: Low

Vulnerable Code

python
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}")

print("✅ Tokens saved to", wahoo_auth.TOKENS_PATH)
print()
print(f"  access_token  : {payload.get('access_token', '')[:16]}…")
print(f"  refresh_token : {payload.get('refresh_token', '')[:16]}…")
print(f"  expires_in    : {payload.get('expires_in')} s")
print(f"  scope         : {payload.get('scope', '')}")

Technical Analysis

The setup helper prints the first 16 characters of both the access token and refresh token. These values are authentication secrets and should not be exposed, even partially. Agent execution transcripts, terminal logging, shell-session recording, CI logs, support captures, and screen sharing may retain the output.

A token prefix is generally insufficient for direct authentication by itself, so the immediate exploitability is limited. Nevertheless, disclosure is unnecessary and contradicts the safer behavior in lib/wahoo_auth.py, which reports only token presence and metadata.

The script also prints the first eight characters of the short-lived authorization code before exchange. While that code is normally invalidated after successful use, printing any credential material is avoidable.

Attack Path

  1. A user or Agent runs scripts/oauth_setup.py in a recorded or remotely observable session.
  2. The script exchanges the authorization code successfully.
  3. Token and authorization-code prefixes are written to standard output.
  4. Another party with access to Agent transcripts, terminal logs, CI output, or screen recordings obtains the fragments.
  5. The fragments can assist secret correlation, log matching, or validation of t ...[truncated 364 chars]
Remediation
View remediation

Remediation Suggestions

  • Never print any portion of access tokens, refresh tokens, client secrets, or authorization codes.
  • Report only whether each token was received, its expiry, granted scope, and the protected destination path.
  • Reuse the redacted-status approach implemented in lib/wahoo_auth.py.
  • Review OAuth error handling to ensure server responses cannot echo submitted secrets into logs.
  • Add automated tests that assert token values and prefixes do not appear in standard output or error output.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (35)

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 10)May include surrounding context.

md
## What you get

- **OAuth2 setup helper** — manual-paste flow that works with any registered callback URL, no local HTTPS server required
- **Auto-refresh** of access tokens via the `offline_data` refresh token
- **API client** — workout list, workout detail, user profile, with built-in 429 backoff
- **FIT downloader** — pulls files from Wahoo's CDN (no API rate-limit cost)
- **FIT parser** — power, cadence, HR, GPS, elevation → JSON / SQLite

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 · SKILL.md (reported line 12)May include surrounding context.

md
## What you get

- **OAuth2 setup helper** — manual-paste flow that works with any registered callback URL, no local HTTPS server required
- **Auto-refresh** of access tokens via the `offline_data` refresh token
- **API client** — workout list, workout detail, user profile, with built-in 429 backoff
- **FIT downloader** — pulls files from Wahoo's CDN (no API rate-limit cost)
- **FIT parser** — power, cadence, HR, GPS, elevation → JSON / SQLite

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 115)May include surrounding context.

md
| List workouts | `GET /v1/workouts?page=N&per_page=N` (default 30) |
| Workout detail | `GET /v1/workouts/:id` (FIT URL at `workout_summary.file.url`) |
| User profile | `GET /v1/user` |
| Access token TTL | ~2 hours |
| Sandbox rate limit | 25 / 5 min, 100 / hr, 250 / day |
| Production rate limit | 200 / 5 min, 1,000 / hr, 5,000 / day |

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 · SKILL.md (reported line 187)May include surrounding context.

md
| List workouts | `GET /v1/workouts?page=N&per_page=N` (default 30) |
| Workout detail | `GET /v1/workouts/:id` (FIT URL at `workout_summary.file.url`) |
| User profile | `GET /v1/user` |
| Access token TTL | ~2 hours |
| Sandbox rate limit | 25 / 5 min, 100 / hr, 250 / day |
| Production rate limit | 200 / 5 min, 1,000 / hr, 5,000 / day |

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The supplied code is a standalone local FIT parser. It reads a provided file path, parses session/record/lap/device/zone data, converts timestamps and GPS semicircles, and optionally prints JSON from a CLI. The declared description includes multiple broader capabilities—Wahoo cloud access, workout retrieval, FIT download, and persistence into SQLite—that are absent from this code chunk. While the parsing portion aligns partially with the description (it does parse power/HR/cadence/GPS-related FIT contents), the declared purpose materially overstates the code’s behavior and primary scope.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The code only provides network access to Wahoo endpoints (/v1/user, /v1/workouts, /v1/workouts/{id}) plus raw FIT file download. The declared description claims a broader workflow: downloading FIT files and parsing workout sensor/GPS data into local SQLite for analysis. Those parsing and database ingestion behaviors are absent from this chunk. While fetching workouts and downloading FIT files are accurately represented, the primary declared end-to-end functionality is overstated relative to the supplied code. Additionally, the code accesses user profile data via /v1/user, which is not reflected in the description.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The supplied code chunk does not implement the declared workout retrieval, FIT download, parsing, or SQLite ingestion behavior. Its purpose is specifically OAuth setup: obtaining authorization from Wahoo, exchanging an auth code for tokens, and persisting those tokens locally. While authentication can be a supporting detail for a Wahoo API integration, this chunk’s primary behavior is credential bootstrap and secret storage, which is not reflected in the declared description. Therefore this code chunk materially differs from the stated purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The code chunk’s actual function is authentication maintenance: it reads Wahoo OAuth credentials from environment variables and/or a local token file, calls the Wahoo token endpoint, and writes refreshed tokens back to disk. That behavior is related to Wahoo API access, but it is materially different from the declared purpose, which specifically describes fetching workouts, downloading FIT files, and parsing metrics into SQLite. Because the supplied code performs a distinct capability not reflected in the description and does not implement the described data retrieval/processing behavior, this chunk is a description-behavior mismatch.

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 · SKILL.md (reported line 128)May include surrounding context.

Returns { id, height, weight, first, last, email, birth, gender, created_at, updated_at }. Height and weight are returned as decimal strings.

Refresh Access Token

bash
bash {baseDir}/scripts/refresh_token.sh

Credential Access

High
Category
Privilege Escalation
Confidence
86% confidence
Finding

By explicitly describing a model where several access tokens can remain valid, the skill normalizes a session-management pattern that can prolong unauthorized access after leakage. In the context of local token persistence and automated refresh, this increases the practical impact of token theft.

Content

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

-d refresh_token="${WAHOO_REFRESH_TOKEN}"

text

The new access token does not invalidate the old one until you successfully use it (Wahoo allows up to 10 unrevoked access tokens per user as of Jan 2026).

## Common Data Fields

Credential Access

High
Category
Privilege Escalation
Confidence
86% confidence
Finding

By explicitly describing a model where several access tokens can remain valid, the skill normalizes a session-management pattern that can prolong unauthorized access after leakage. In the context of local token persistence and automated refresh, this increases the practical impact of token theft.

Content

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

-d refresh_token="${WAHOO_REFRESH_TOKEN}"

text

The new access token does not invalidate the old one until you successfully use it (Wahoo allows up to 10 unrevoked access tokens per user as of Jan 2026).

## Common Data Fields

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
#!/usr/bin/env bash
# Refresh the Wahoo access token using the refresh_token. Reads tokens from
# ~/.openclaw/secrets/wahoo_tokens.json (preferred) or env vars, and writes
# the new tokens back to the same JSON file.

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
#!/usr/bin/env bash
# Refresh the Wahoo access token using the refresh_token. Reads tokens from
# ~/.openclaw/secrets/wahoo_tokens.json (preferred) or env vars, and writes
# the new tokens back to the same JSON file.

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 166)May include surrounding context.

python
#!/usr/bin/env bash
# Refresh the Wahoo access token using the refresh_token. Reads tokens from
# ~/.openclaw/secrets/wahoo_tokens.json (preferred) or env vars, and writes
# the new tokens back to the same JSON file.

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 · scripts/refresh_token.sh (reported line 2)May include surrounding context.

sh
#!/usr/bin/env bash
# Refresh the Wahoo access token using the refresh_token. Reads tokens from
# ~/.openclaw/secrets/wahoo_tokens.json (preferred) or env vars, and writes
# the new tokens back to the same JSON file.

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 27)May include surrounding context.

md
### 1. Register a Wahoo developer app

1. Go to [developers.wahooligan.com](https://developers.wahooligan.com).
2. Create an application. Sandbox is automatic — no review required.
3. Set the callback URL (e.g. `https://localhost:8080/`). The OAuth helper uses manual paste, so any URL you can reach in a browser works.
4. Request scopes. Minimum useful set: `workouts_read offline_data user_read`. Add `power_zones_read plans_read routes_read` if you want zones / plans / routes.
5. Note your **Client ID** and **Client Secret**.

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 · README.md (reported line 144)May include surrounding context.

md
- `client_id` and `client_secret` come from environment variables, or `~/.openclaw/secrets/wahoo.env` (auto-loaded by `wahoo_auth.py` if env is empty), or `~/.clawdbot/clawdbot.json` — never hardcoded in source. Never paste these into agent chat windows or third-party tools; provide them only to local config files you control.
- FIT files live locally; nothing is uploaded anywhere unless you explicitly add an upload step.
- `.gitignore` blocks `*.env`, `*.db`, `*.fit`, and `*tokens*.json` — verify before committing if you fork.
- The local `wahoo.db` and `wahoo_fit/` directory contain GPS, heart-rate, and power history. They inherit your training-dir permissions (default `~/.openclaw/workspace/training/`). Tighten with `chmod 700 ~/.openclaw/workspace/training` if other local users share the machine.
- `python3 lib/wahoo_auth.py` prints redacted token-file status only (presence, expiry, scope). It never echoes the access or refresh token values.

## Contributing

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill advertises and instructs use of capabilities including shell execution, network access, environment-variable access, and local file read/write, but it does not declare any explicit tool scope or permission boundaries. In an agent setting, this weakens least-privilege controls and makes it easier for the skill to access secrets, write tokens to disk, and transmit user data without clear policy enforcement.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill describes automatic loading of client credentials from a local secrets file and presents this as convenience, but it does not warn users about the sensitivity of those secrets or the need for restrictive file permissions. In an agent environment, silent secret loading increases the chance of accidental credential exposure, misuse by other local processes, or unintended use on behalf of a user.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
84% confidence
Finding

The skill explicitly requests the offline_data scope, which enables long-lived refresh-token-based access and therefore persistent sessions beyond a single interactive login. That is often necessary for sync tools, but it materially increases risk in an agent context because compromise of the stored refresh token can provide ongoing access to a user's workout history and possibly related personal data.

Content

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

md
### 1. Register a Wahoo Developer App

1. Go to https://developers.wahooligan.com
2. Create an application (Sandbox is automatic — no review)
3. Set callback URL (e.g. `https://localhost:8080/` — the manual-paste OAuth helper works with any registered callback)
4. Request scopes: `workouts_read offline_data user_read` (add `power_zones_read plans_read routes_read` if you want zones/plans/routes)
5. Note your **Client ID** and **Client Secret**

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The OAuth flow writes refresh and access tokens to disk in a predictable location without any explicit warning about local persistence, file permissions, or token theft risk. Because refresh tokens can provide long-lived account access, storing them on disk without clear security guidance materially increases the risk of session compromise if the host is shared or malware is present.

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 · SKILL.md (reported line 96)May include surrounding context.

List Workouts (paginated)

bash
curl -s -H "Authorization: Bearer ${WAHOO_ACCESS_TOKEN}" \
  "https://api.wahooligan.com/v1/workouts?page=1&per_page=30"

External Transmission

Medium
Category
Data Exfiltration
Confidence
79% confidence
Finding

The skill includes retrieval of detailed user profile data including email, birth date, height, weight, and gender, which is more sensitive than workout metadata. Because the description emphasizes workout sync and analysis, inclusion of profile access broadens privacy exposure and may lead agents to collect personal data beyond what is necessary for the primary task.

Content

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

bash
curl -s -H "Authorization: Bearer ${WAHOO_ACCESS_TOKEN}" \
  "https://api.wahooligan.com/v1/user"

Returns { id, height, weight, first, last, email, birth, gender, created_at, updated_at }. Height and weight are returned as decimal strings.

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 · SKILL.md (reported line 97)May include surrounding context.

md
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 · SKILL.md (reported line 106)May include surrounding context.

md
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"

Static analysis

No suspicious patterns detected.