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

Content
View full analysis

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
View 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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/whoop_fetch.py (reported line 52)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/whoop_fetch.py (reported line 66)May include surrounding context.

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

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.

Content

No source excerpt is available for this finding.

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.

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

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

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/whoop_auth.py (reported line 151)May include surrounding context.

python
### 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

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/whoop_auth.py (reported line 152)May include surrounding context.

python
### 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

Credential Access

High
Category
Privilege Escalation
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.

Content

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

md
## 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`

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/whoop_auth.py (reported line 165)May include surrounding context.

python
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

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

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

md
## 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`

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

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 · references/api_endpoints.md (reported line 3)May include surrounding context.

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

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 · scripts/whoop_auth.py (reported line 20)May include surrounding context.

python
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"]

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 · scripts/whoop_auth.py (reported line 21)May include surrounding context.

python
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"]

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 · scripts/whoop_auth.py (reported line 22)May include surrounding context.

python
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"]

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 · scripts/whoop_fetch.py (reported line 19)May include surrounding context.

python
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"]

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 · scripts/whoop_fetch.py (reported line 20)May include surrounding context.

python
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"]

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.