Back to skill

Security audit

WHOOP Health

Security checks for vulnerabilities and agentic risk

Overview

The skill appears intended for legitimate WHOOP data access, but it handles sensitive health/account data with broad OAuth scopes and an insecure OAuth callback implementation that users should review before installing.

Install only if you are comfortable granting this skill broad read access to WHOOP recovery, sleep, workout, cycle, profile, and body-measurement data. Protect ~/.whoop_tokens.json, avoid shared or synced machines, revoke access when finished, and be aware that the OAuth flow should be hardened with state validation before use in a sensitive 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
scripts/whoop_auth.py:78
Finding
OAuth Callback Accepts Authorization Codes Without State Validation## Vulnerability Details **File Location**: `scripts/whoop_auth.py`, lines 78–115 **Vulnerability Type**: OAuth login CSRF and account confusion caused by missing `state` validation **Risk Level**: Medium **Vulnerable Code**: ```python def run_auth_flow(client_id, client_secret): state = secrets.token_urlsafe(16) auth_code_holder = {} class CallbackHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): parsed = urllib.parse.urlparse(self.path) if parsed.path == "/callback": params = urllib.parse.parse_qs(parsed.query) auth_code_holder["code"] = params.get("code", [None])[0] auth_code_holder["error"] = params.get("error", [None])[0] self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(b"<h2>Authorization complete. You can close this tab.</h2>") def log_message(self, format, *args): pass server = http.server.HTTPServer(("localhost", 8080), CallbackHandler) thread = threading.Thread(target=server.handle_request) thread.start() params = urllib.parse.urlencode({ "client_id": client_id, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": SCOPES, "state": state, }) ``` ### Technical Analysis The authentication flow generates a cryptographically random OAuth `state` value and includes it in the authorization request. However, the callback handler only extracts `code` and `error`; it neither retrieves the returned `state` parameter nor compares it with the value generated for the active authentication flow. OAuth `state` binds an authorization response to the browser session that initiated it. Merely sending the parameter does not provide protection. The callback must reject resp ...[truncated 2339 chars]
Remediation
## Remediation Suggestions 1. Extract the callback's `state` value and require an exact match with the locally generated value before accepting either an authorization code or an OAuth error. 2. Use `secrets.compare_digest()` for the comparison and reject missing or multi-valued state parameters. 3. Return an HTTP error response for mismatched callbacks and continue waiting for a valid callback until the authentication timeout expires. 4. Accept only one valid callback and explicitly close the HTTP server after completion or timeout. 5. Add PKCE using an S256 code challenge and verifier so an injected or intercepted authorization code cannot be redeemed without the locally generated verifier. 6. Avoid placing unnecessary authorization details in console output, and clearly report rejected callback attempts without logging codes or tokens. Example hardening: ```python returned_state = params.get("state", [None])[0] expected_state = state if ( returned_state is None or not secrets.compare_digest(returned_state, expected_state) ): self.send_response(400) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"Invalid OAuth state.") return code_values = params.get("code", []) if len(code_values) != 1 or not code_values[0]: self.send_response(400) self.end_headers() return auth_code_holder["code"] = code_values[0] ```
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (24)

Tainted flow: 'req' from os.environ.get (line 50, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
}).encode()
            req = urllib.request.Request(TOKEN_URL, data=data, method="POST")
            req.add_header("Content-Type", "application/x-www-form-urlencoded")
            with urllib.request.urlopen(req) as resp:
                tokens = json.loads(resp.read())
                tokens["obtained_at"] = int(time.time())
                TOKEN_FILE.write_text(json.dumps(tokens, indent=2))
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 50, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
}).encode()
            req = urllib.request.Request(TOKEN_URL, data=data, method="POST")
            req.add_header("Content-Type", "application/x-www-form-urlencoded")
            with urllib.request.urlopen(req) as resp:
                tokens = json.loads(resp.read())
                tokens["obtained_at"] = int(time.time())
                TOKEN_FILE.write_text(json.dumps(tokens, indent=2))
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill metadata and documentation present inconsistent API versioning and data handling claims, while also expanding collection to profile and body measurement data not clearly disclosed in the main description. This can mislead users about what data is accessed and create consent and privacy issues around especially sensitive health-related information.

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding
The skill metadata and documentation present inconsistent API versioning and data handling claims, while also expanding collection to profile and body measurement data not clearly disclosed in the main description. This can mislead users about what data is accessed and create consent and privacy issues around especially sensitive health-related information.

Credential Access

High
Category
Privilege Escalation
Content
### 2. Authenticate

Run the OAuth helper script to get an access token:

```bash
python3 scripts/whoop_auth.py --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. Authenticate

Run the OAuth helper script to get an access token:

```bash
python3 scripts/whoop_auth.py --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. Authenticate

Run the OAuth helper script to get an access token:

```bash
python3 scripts/whoop_auth.py --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Token Management

Tokens are stored at `~/.whoop_tokens.json`. The fetch script auto-refreshes using the refresh token when the access token expires (24h lifetime).

To revoke access: `python3 scripts/whoop_auth.py --revoke`
Confidence
86% confidence
Finding
The skill states that access and refresh tokens are stored in a predictable local file and automatically reused for refresh. Persistent credential storage is dangerous because anyone with filesystem access, backups, logs, or malware on the host may obtain long-lived tokens and access the user's WHOOP account and associated sensitive data.

Credential Access

High
Category
Privilege Escalation
Content
expires_in = tokens.get("expires_in", 3600)
            expires_at = obtained + expires_in
            remaining = expires_at - int(time.time())
            print(f"Access token: {'valid' if remaining > 0 else 'EXPIRED'} ({max(0, remaining)}s remaining)")
            print(f"Refresh token: {'present' if 'refresh_token' in tokens else 'missing'}")
        return
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill advertises shell, network, file read/write, and environment access without declaring any explicit tool scope or permissions boundary. That increases the blast radius of the skill because an agent could invoke broader capabilities than a user would reasonably expect when handling sensitive health and OAuth data.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The trigger language is overly broad and may cause the skill to activate for nearly any WHOOP wearable-analysis request, even when the user did not intend to authorize account connection or health-data retrieval. In the context of a health-data skill, over-triggering is more dangerous because it can lead to unnecessary handling of sensitive personal information.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill handles sensitive health, profile, and body measurement data but does not warn users about the privacy implications of retrieving and storing that information. In a health context, missing privacy disclosure is significant because users may expose regulated or highly personal data without informed consent.

Session Persistence

Medium
Category
Rogue Agent
Content
## Setup

### 1. Create a Developer App

1. Go to [developer.whoop.com](https://developer.whoop.com) and sign in with your WHOOP account
2. Create a new application; set Redirect URI to `http://localhost:8080/callback`
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.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The documentation tells users that tokens are saved locally but does not prominently warn that OAuth access and refresh tokens are sensitive credentials whose compromise permits continued account access. Storing long-lived refresh material on disk without a strong warning increases the chance of accidental exposure through backups, shared systems, or weak file permissions.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This markdown file documents authenticated API access to personally identifiable information and health-related metrics such as email, body measurements, recovery, sleep, and workout data. Under the markdown-specific warning rule, the description should include a user-facing warning about privacy-sensitive data handling, but no such warning appears anywhere in the file.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
from pathlib import Path

TOKEN_FILE = Path.home() / ".whoop_tokens.json"
BASE_URL = "https://api.prod.whoop.com/developer/v1"
TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

VALID_TYPES = ["recovery", "sleep", "workout", "cycle", "profile", "body_measurement"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script exports sensitive health data and profile/body measurements to local JSON or CSV files without warning the user, restricting permissions, or offering safer defaults. On multi-user systems or synced/home-backed environments, these files may be exposed to other users, backup services, or unintended tooling, increasing privacy and compliance risk.

Context-Inappropriate Capability

Low
Confidence
84% confidence
Finding
The manifest frames the skill as connecting to WHOOP and retrieving/analyzing wearable data. This helper script additionally sources sensitive OAuth client credentials from process environment variables, which is a capability not mentioned in the manifest and is not directly part of data analysis itself.

Intent-Code Divergence

Low
Confidence
97% confidence
Finding
The module docstring says it 'Fetches health data from WHOOP API v1' (L003-L005), but the configured base URL and token URL point to developer and OAuth endpoints that do not match that stated API versioning (L019-L020). This is an intent/documentation mismatch because the inline documentation describes a different API target than the code actually uses.

Static analysis

No suspicious patterns detected.