Back to skill

Security audit

WHOOP (Official API)

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it says for WHOOP reporting, but it handles sensitive health data and tokens with weak safeguards and ships an under-disclosed partner API surface with privileged write operations.

Review this skill before installing. It can store WHOOP OAuth tokens locally, fetch and persist sensitive health/profile/body data, send summaries to chat channels, and create scheduled pushes. Use a private token/output directory, narrow WHOOP_SCOPES before login, avoid shared /tmp output paths, and do not use or expose the bundled partner API operations unless you explicitly need and control those credentials.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/whoop_oauth_login.py:52
Finding
OAuth Authorization Flow Lacks State and Callback Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_oauth_login.py:52-72` **Vulnerability Type**: OAuth login CSRF and account-binding confusion **Risk Level**: High ### Vulnerable Code ```python params = { "client_id": client_id, "redirect_uri": redirect_uri, "response_type": "code", "scope": scopes, } auth_url = OAUTH_AUTH_URL + "?" + urllib.parse.urlencode(params) print("Open this URL in a browser and approve access:\n") print(auth_url) print("\nAfter approval, paste either the full redirect URL or just the code:") user_in = input("> ") code = parse_code(user_in) tok = exchange_code_for_token( code=code, client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri, ) ``` The associated parser at `scripts/whoop_oauth_login.py:34-44` only extracts the authorization code: ```python def parse_code(user_input: str) -> str: s = user_input.strip() if s.startswith("http://") or s.startswith("https://"): u = urllib.parse.urlparse(s) q = urllib.parse.parse_qs(u.query) code = (q.get("code") or [None])[0] if code: return code raise SystemExit("Could not find ?code= in the pasted redirect URL") return s ``` ### Technical Analysis The authorization request does not include a cryptographically random OAuth `state` parameter. The callback parser accepts a redirect URL from any HTTP or HTTPS origin, extracts only its `code`, and does not verify that its scheme, host, port, and path match the configured `WHOOP_REDIRECT_URI`. The authorization code is therefore not bound to the login transaction initiated by this process. Accepting a bare code further prevents callback-origin and state validation. This creates login CSRF or account-binding confusion risk: a valid authorization code generated through a different browser transaction can be submitted and stored by the Skill. PKCE is also absent. Although the client secret is required for exchang ...[truncated 1237 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a fresh state value for every authorization attempt: ```python import secrets expected_state = secrets.token_urlsafe(32) params["state"] = expected_state ``` 2. Require the user to provide the complete callback URL rather than permitting a bare authorization code. 3. Parse the callback and compare `state` using `secrets.compare_digest`. 4. Compare the callback scheme, hostname, effective port, and path against `WHOOP_REDIRECT_URI` before accepting the code. 5. Reject callbacks containing OAuth `error` fields, missing state, duplicate state values, or multiple authorization codes. 6. Keep transaction state only for the short duration of the login flow and invalidate it after one use. 7. Add PKCE with a random verifier and `S256` challenge if supported by WHOOP. 8. Prefer a temporary localhost callback listener that validates the callback automatically instead of manual copy-and-paste. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/whoop_token.py:46
Finding
Sensitive Token and Health Data Files Are Created Through Predictable, Symlink-Following Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_token.py:46-55`; related locations `scripts/whoop_fetch.py:228-232` and `scripts/whoop_normalize.py:207-208` **Vulnerability Type**: Unsafe temporary files, permissive sensitive-data output, and symlink overwrite **Risk Level**: High ### Vulnerable Code Token storage uses a deterministic `.tmp` pathname and restricts permissions only after replacement: ```python def save_token(token: Dict[str, Any], path: Optional[str] = None) -> str: p = path or token_path() _mkdirp(p) tmp = p + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(token, f, indent=2, sort_keys=True) f.write("\n") os.replace(tmp, p) try: os.chmod(p, 0o600) except PermissionError: pass return p ``` Raw health data is written using ordinary file creation: ```python out_path = args.out with open(out_path, "w", encoding="utf-8") as f: json.dump(bundle, f, indent=2, sort_keys=True) f.write("\n") ``` Normalized data is also written without an explicit restrictive mode: ```python json.dump(out, open(args.out, "w", encoding="utf-8"), indent=2, sort_keys=True) open(args.out, "a", encoding="utf-8").write("\n") ``` The documented workflow uses predictable shared temporary paths: ```bash python3 scripts/whoop_fetch.py --date today --out /tmp/whoop_raw_today.json python3 scripts/whoop_normalize.py /tmp/whoop_raw_today.json --out /tmp/whoop_today.json ``` ### Technical Analysis The token writer creates `token.json.tmp` using `open(..., "w")`. This follows an existing symbolic link and initially uses permissions derived from the process umask. The final token file is changed to mode `0600` only after all token material has been written and moved. The temporary file can consequently expose access and refresh tokens before replacement when its containing directory is accessible and the umask is permissive. The raw and normalized outputs similarly inherit ...[truncated 2017 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the token directory with mode `0700` and verify that it is owned by the current user. 2. Create temporary files in the destination directory with unpredictable names and mode `0600`, for example with `tempfile.NamedTemporaryFile`. 3. Flush and `fsync` the secure temporary file before atomically replacing the destination. 4. Use no-follow and exclusive-creation protections where supported, such as `O_NOFOLLOW`, `O_EXCL`, and `O_CREAT`. 5. Validate that existing destinations are regular files and reject symbolic links. 6. Apply mode `0600` at creation time, not after sensitive content has been written. 7. Treat a failure to set secure permissions as fatal rather than silently ignoring it. 8. Apply the same secure atomic writer to raw and normalized WHOOP data. 9. Replace predictable shared `/tmp` examples with files inside a private `0700` directory created through `tempfile.mkdtemp()` or an application-specific private data directory. 10. Document data-retention and secure-deletion expectations for generated health records. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/whoop_oauth_login.py:24
Finding
Default OAuth Scopes and Unconditional Fetching Exceed Least-Privilege Requirements<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_oauth_login.py:24`; related location `scripts/whoop_fetch.py:201-208` **Vulnerability Type**: Excessive OAuth scopes and unnecessary sensitive-data collection **Risk Level**: Medium ### Vulnerable Code The login flow requests every supported read scope by default: ```python DEFAULT_SCOPES = "read:recovery read:sleep read:cycles read:workout read:profile read:body_measurement" ``` Every execution of the fetcher retrieves profile and body-measurement resources, regardless of the requested report: ```python # Non-collection endpoints bundle["endpoints"]["profile_basic"] = { "url": url_with_params("/v2/user/profile/basic"), "data": http_get_json(url_with_params("/v2/user/profile/basic"), access_token), } bundle["endpoints"]["body_measurement"] = { "url": url_with_params("/v2/user/measurement/body"), "data": http_get_json(url_with_params("/v2/user/measurement/body"), access_token), } ``` The normalized report retains profile identity fields: ```python "profile": { "name": prof.get("name") or prof.get("full_name"), "email": prof.get("email"), }, ``` ### Technical Analysis Common daily recovery summaries require only a subset of the available scopes, typically recovery, sleep, and cycles. Nevertheless, the default authorization request also asks for workout, profile, and body-measurement access. The fetcher then always retrieves profile and body measurements, even though `whoop_render.py` does not use those values in its human-readable summary. This violates least-privilege and data-minimization principles. It enlarges the authorization represented by a stolen token and increases the amount of sensitive information stored in raw output files. Although users can override `WHOOP_SCOPES`, secure behavior should be the default and should be selected according to the requested workflow. ### Attack Path 1. A user asks for a basic recovery, sleep, or strain summary. 2. The ...[truncated 1117 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Change the default scope set to the minimum required for the basic workflow, such as recovery, sleep, and cycles. 2. Offer explicit workflow or endpoint flags, for example: - `--include-workouts` - `--include-profile` - `--include-body-measurement` 3. Derive requested OAuth scopes from the selected features before authorization. 4. Fetch profile and body-measurement endpoints only when explicitly requested and authorized. 5. Do not place unused endpoint responses in the raw output bundle. 6. Omit email and other identity fields from normalized output unless the user specifically requests them. 7. Display the requested scopes and data categories to the user before opening the authorization URL. 8. Document how users can revoke access and reauthorize with a reduced scope set. ]]>
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
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (34)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description presents a connector/fetcher skill responsible for authenticating with WHOOP, managing tokens, retrieving WHOOP v2 data, and enabling downstream reporting or chat delivery. The supplied code does none of that. It only processes an already-fetched raw JSON bundle from a file, picks best-matching records by date range, computes a compact summary, and writes output to another file. While the normalized fields relate to WHOOP data domains mentioned in the description, this chunk’s actual purpose is materially narrower and different: offline normalization, not OAuth connection or data retrieval. Therefore the description does not accurately represent this code chunk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared description presents a full WHOOP connector/integration with authentication, token lifecycle management, remote API access, and chat update use cases. The actual code chunk is much narrower: it parses an input JSON file and renders a summary string with channel-specific formatting conventions. While the rendered fields relate to WHOOP metrics, the code does not implement the core declared capabilities. This is a material description-to-behavior mismatch rather than a minor implementation detail.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
4) The script stores tokens at `WHOOP_TOKEN_PATH`.

If you need to revoke later, use `delete /v2/user/access` (see `references/whoop_api.md`).

## Workflow 2 — Fetch metrics (today / yesterday)
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The OpenAPI spec exposes trusted-partner endpoints for lab requisitions, service requests, token issuance, status updates, and diagnostic report uploads even though the skill description only justifies end-user WHOOP OAuth and fitness metric retrieval/reporting. This creates a capability mismatch that could let the agent request or operate on partner-only medical workflow data if corresponding credentials are ever available, expanding the attack surface far beyond the declared purpose.

Credential Access

High
Category
Privilege Escalation
Content
"post" : {
        "tags" : [ "Partner" ],
        "summary" : "Request a partner client token",
        "description" : "Exchanges partner client credentials for an access token.",
        "operationId" : "requestToken",
        "requestBody" : {
          "content" : {
Confidence
95% confidence
Finding
This endpoint explicitly exchanges partner client credentials for an access token, introducing handling of highly sensitive non-user secrets and privileged bearer tokens. In a consumer-facing skill meant for WHOOP OAuth data access, unnecessary support for privileged credential flows raises the risk of secret leakage, token theft, and unauthorized partner API use.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The service-request status update endpoint allows modification of external workflow state, which is not justified by the stated use case of connecting to WHOOP and summarizing metrics in chat. Such unnecessary write access increases the chance that prompt injection, agent misuse, or misconfiguration could alter partner records or operational processes.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The diagnostic report upload endpoint is a state-changing medical-data capability unrelated to reading WHOOP recovery, sleep, cycle, workout, profile, or body measurements. In an agent context, retaining this operation means the skill could be induced to create or alter external clinical records, causing integrity and compliance risks well beyond passive fitness reporting.

Credential Access

High
Category
Privilege Escalation
Content
"/v2/user/access" : {
      "delete" : {
        "tags" : [ "User" ],
        "description" : "Revoke the access token granted by the user. If the associated OAuth client is configured to receive webhooks, it will no longer receive them for this user.",
        "operationId" : "revokeUserOAuthAccess",
        "responses" : {
          "204" : {
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- Workout collection: `GET /v2/activity/workout`
- Basic profile: `GET /v2/user/profile/basic`
- Body measurements: `GET /v2/user/measurement/body`
- Revoke access: `DELETE /v2/user/access`

## Pagination
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Credential Access

High
Category
Privilege Escalation
Content
"""WHOOP token utilities.

- Store tokens in a local JSON file.
- Refresh access tokens when expired.

No third-party deps.
"""
Confidence
84% confidence
Finding
The file explicitly stores OAuth access and refresh tokens in a local JSON file, creating a credential exposure risk if the host is multi-user, backups are accessible, or the token path is redirected to an insecure location. Although file mode 0600 is applied after saving, the design still relies on plaintext token-at-rest storage and trusts an environment-controlled path.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill documentation instructs use of shell, environment variables, local file storage, and networked OAuth/API access, but it declares no explicit tool scope or allowed-tools boundary. In an agent environment, missing scope restrictions can let the skill be invoked with broader capabilities than necessary, increasing the blast radius if the skill is misused or prompt-injected.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The description includes broad activation phrases like generating reports and sending updates to any chat channel, which could cause the skill to trigger on common health-summary or messaging requests beyond clearly WHOOP-specific intent. Over-broad invocation increases the chance an agent routes unrelated user tasks into a skill that accesses stored OAuth tokens and external data.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill handles OAuth client secrets and stores access/refresh tokens locally, but it gives no explicit warning about the sensitivity of those credentials, file permissions, or the risk of exposing redirect URLs and token files. In a multi-user or poorly secured environment, this can lead to credential theft and unauthorized access to personal health data.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The partner token endpoint accepts client credentials and returns an access token, yet the skill description contains no warning that especially sensitive secrets may be handled if this capability is enabled. In an agent ecosystem, undocumented secret-handling paths increase the risk of accidental exposure, unsafe storage, or misuse of privileged partner credentials.

Vague Triggers

Medium
Confidence
90% confidence
Finding
The spec includes state-changing partner operations that can update service-request status and upload results, but the skill description does not warn that using the skill may modify external records. This omission is risky because users may expect read-only WHOOP reporting while the agent actually possesses write-capable actions against third-party systems.

Vague Triggers

Medium
Confidence
86% confidence
Finding
These schemas document access to privacy-sensitive health and identity data such as recovery scores, HRV, resting heart rate, sleep stages, body measurements, and profile email/name, but the skill description does not prominently warn users about the sensitivity of the data being accessed and stored. In a chat-oriented skill that can also send updates to external channels, inadequate disclosure increases the risk of uninformed consent and oversharing of health information.

External Transmission

Medium
Category
Data Exfiltration
Content
"type" : "oauth2",
        "flows" : {
          "clientCredentials" : {
            "tokenUrl" : "https://api.prod.whoop.com/developer/v2/partner/token",
            "scopes" : {
              "whoop-partner/token" : "Read service requests and upload results."
            }
Confidence
78% confidence
Finding
While a token URL alone is not usually a vulnerability, this specific one exposes a trusted-partner client-credentials flow that is out of scope for the declared fitness-reporting skill. In context, it signals availability of privileged non-user credentials and partner-only access paths that materially increase risk if included unnecessarily.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The code silently defaults the timezone to Asia/Shanghai when neither --tz nor requested_tz is provided, which can cause health data to be normalized against the wrong day boundary. In this skill, that can misattribute sleep, recovery, cycle, and workout records to the wrong date, leading to inaccurate reports or summaries and possible privacy surprises if data is sent to other chat channels under an incorrect day context.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script writes a normalized JSON file containing personal profile fields such as name and email, plus health-derived WHOOP metrics, directly to disk without any consent check, minimization, redaction, or storage protection. In the context of a fitness/health integration, this creates a privacy and data-handling risk because sensitive personal and wellness data may persist on disk, be copied into logs/backups, or be read by other local users or processes.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code saves an OAuth token JSON file to a local path, which is a safety-relevant credential-handling operation. Although the module docstring notes that it writes a token file, the interactive flow does not warn the user before saving sensitive credentials or explain the storage implications at the point of action.

External Transmission

Medium
Category
Data Exfiltration
Content
from typing import Any, Dict, Optional


OAUTH_AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
OAUTH_TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/openclaw/whoop/token.json")
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 typing import Any, Dict, Optional


OAUTH_AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
OAUTH_TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/openclaw/whoop/token.json")
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 typing import Any, Dict, Optional


OAUTH_AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
OAUTH_TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/openclaw/whoop/token.json")
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 typing import Any, Dict, Optional


OAUTH_AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
OAUTH_TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/openclaw/whoop/token.json")
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 typing import Any, Dict, Optional


OAUTH_AUTH_URL = "https://api.prod.whoop.com/oauth/oauth2/auth"
OAUTH_TOKEN_URL = "https://api.prod.whoop.com/oauth/oauth2/token"

DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/openclaw/whoop/token.json")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.