Back to skill

Security audit

WHOOP (Official API)

Security checks for vulnerabilities and agentic risk

Overview

This WHOOP skill has a coherent purpose, but it handles sensitive health data and OAuth tokens with under-scoped defaults and weak local storage practices that deserve careful review before installation.

Install only if you are comfortable granting broad WHOOP read access and storing tokens plus health summaries on this machine. Set WHOOP_SCOPES and WHOOP_TZ deliberately, avoid /tmp for outputs, restrict token/output file permissions, and require explicit confirmation before sending or scheduling WHOOP summaries to external chat channels.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/whoop_oauth_login.py:37
Finding
OAuth Authorization Flow Lacks State Validation and PKCE Binding<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_oauth_login.py:37-47, 57-76, 119-142`; `scripts/whoop_token.py:89-100` **Vulnerability Type**: OAuth login CSRF and authorization-response substitution **Risk Level**: High ### Vulnerable 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 ``` ```python q = urllib.parse.parse_qs(parsed.query) code = (q.get("code") or [None])[0] if code: got["code"] = 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) ``` ```python tok = exchange_code_for_token( code=code, client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri, ) ``` ```python def exchange_code_for_token(*, code: str, client_id: str, client_secret: str, redirect_uri: str) -> Dict[str, Any]: tok = _post_form( OAUTH_TOKEN_URL, { "grant_type": "authorization_code", "code": code, "client_id": client_id, "client_secret": client_secret, "redirect_uri": redirect_uri, }, ) _annotate_expiry(tok) return tok ``` ### Technical Analysis The authorization request does not include a cryptographically random OAuth `state` parameter. Neither the copy-and-paste callback parser nor the loopback HTTP handler validates that a callback belongs to the authorization request initiated by the current process. The implementation also does not use PKCE. It therefore has no `code_challenge` in the authorization request and no corresp ...[truncated 1767 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically random `state` value using `secrets.token_urlsafe()`. 2. Store the expected state only for the lifetime of the pending login. 3. Include `state` in the WHOOP authorization URL. 4. Require the callback to contain exactly the expected state and reject missing, mismatched, expired, or reused values. 5. Implement PKCE with a high-entropy `code_verifier` and an S256 `code_challenge`. 6. Include `code_challenge` and `code_challenge_method=S256` in the authorization request. 7. Include the original `code_verifier` in the token exchange. 8. Apply identical state and PKCE validation to both copy-and-paste and loopback modes. 9. Reject OAuth error callbacks explicitly and enforce a short expiration time for pending login state. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/whoop_fetch.py:228
Finding
Sensitive Health Data Is Written Through Predictable and Insufficiently Protected Output Files<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:35-37, 84-85`; `scripts/whoop_fetch.py:228-231`; `scripts/whoop_normalize.py:180-208` **Vulnerability Type**: Unsafe temporary files and plaintext sensitive-data storage **Risk Level**: Medium ### Vulnerable Code The documented workflow recommends predictable files in a shared temporary directory: ```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 ``` The raw API bundle is written with ordinary, symlink-following file creation and umask-dependent permissions: ```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") ``` The normalized document includes identity and health data and is written in the same manner: ```python out: Dict[str, Any] = { "date": requested_date, "timezone": tz, "generated_at": datetime.utcnow().isoformat() + "Z", "profile": { "name": prof.get("name") or prof.get("full_name"), "email": prof.get("email"), }, "recovery": { "score": (best_rec or {}).get("score"), "hrv_ms": (best_rec or {}).get("hrv_ms") or (best_rec or {}).get("hrv"), "rhr_bpm": (best_rec or {}).get("resting_heart_rate") or (best_rec or {}).get("rhr"), }, "sleep": { "duration_minutes": (best_slp or {}).get("duration") or (best_slp or {}).get("duration_minutes"), "performance_percent": (best_slp or {}).get("performance") or (best_slp or {}).get("performance_percent"), }, "cycle": { "strain": (best_cyc or {}).get("strain"), "avg_hr_bpm": (best_cyc or {}).get("average_heart_rate") or (best_cyc or {}).get("avg_heart_rate"), }, "workout": { "count": len(wkos_for_range) if (range_start and range_end) else len(wkos), "top_strain": top_strain, }, "source": { ...[truncated 2158 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Stop recommending fixed filenames in a shared temporary directory. 2. Store sensitive outputs in a private directory owned by the user and set that directory to mode `0700`. 3. Create output files atomically with mode `0600`, using low-level flags such as `O_CREAT`, `O_EXCL`, and, where supported, `O_NOFOLLOW`. 4. Validate that an existing destination is a regular file owned by the current user before replacing it. 5. Write to a securely created temporary file in the destination directory, flush and `fsync` it, and then perform an atomic replacement. 6. Apply the same secure writer to raw and normalized outputs. 7. Minimize raw-data retention and securely remove temporary raw bundles after normalization when the user does not request persistence. 8. Document that WHOOP output contains sensitive health information and must not be placed in shared or publicly readable locations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/whoop_token.py:32
Finding
OAuth Tokens Are Initially Written to an Insecure Predictable Temporary File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_token.py:32-57` **Vulnerability Type**: Insecure temporary token-file creation and symlink following **Risk Level**: Medium ### Vulnerable Code ```python def _mkdirp(path: str) -> None: Path(path).parent.mkdir(parents=True, exist_ok=True) def token_path() -> str: return os.environ.get("WHOOP_TOKEN_PATH", DEFAULT_TOKEN_PATH) def load_token(path: Optional[str] = None) -> Dict[str, Any]: p = path or token_path() with open(p, "r", encoding="utf-8") as f: return json.load(f) 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 ``` ### Technical Analysis The final token path is changed to mode `0600`, but only after credentials have already been written to `p + ".tmp"` and moved into place. The temporary file: - Has a predictable name. - Is opened with umask-dependent permissions. - Is created without exclusive or no-follow flags. - May remain on disk if the process fails before replacement. - Can follow an existing symbolic link. - Is placed in a parent directory whose permissions are not explicitly restricted to `0700`. The token JSON can contain both an access token and a long-lived refresh token. Applying `chmod` only after `os.replace` does not protect the temporary file during creation and writing. ### Attack Path 1. An OAuth login or token refresh invokes `save_token`. 2. In a shared or insufficiently protected configured token directory, a local attacker predicts the `.tmp` filename. 3. The attacker observes the temporary file while it has permissive mode, or pre-creates the path as a symbolic link where filesystem permissions permit. 4. ...[truncated 715 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the token parent directory with mode `0700` and verify that it is owned by the current user. 2. Create the temporary token file atomically with mode `0600` before writing any credential bytes. 3. Use a randomized temporary filename in the destination directory rather than the fixed `.tmp` suffix. 4. Use `O_EXCL` and `O_NOFOLLOW` where supported, and verify that the created object is a regular file. 5. Flush and `fsync` the token file before atomically replacing the final path. 6. Clean up the temporary file in a `finally` block after failures. 7. Verify final-file ownership and permissions rather than silently ignoring all inability to enforce the intended protection. 8. Consider platform-backed credential storage instead of plaintext JSON when available. ]]>

T05 · Unauthorized Access and Privilege Escalation

Note
Location
scripts/whoop_oauth_login.py:27
Finding
Default Workflow Requests and Retrieves More Personal Data Than Required<![CDATA[ ## Vulnerability Details **File Location**: `scripts/whoop_oauth_login.py:27`; `scripts/whoop_fetch.py:200-223`; `scripts/whoop_normalize.py:118-119, 180-184`; `scripts/whoop_render.py:39-79` **Vulnerability Type**: Excessive OAuth scopes and unnecessary sensitive-data collection **Risk Level**: Low ### Vulnerable Code All available read scopes are requested by default: ```python DEFAULT_SCOPES = "read:recovery read:sleep read:cycles read:workout read:profile read:body_measurement" ``` Every fetch retrieves profile and body-measurement data regardless of which summary fields are needed: ```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), } # Collections with date filtering + pagination collections = { "recovery": "/v2/recovery", "sleep": "/v2/activity/sleep", "cycle": "/v2/cycle", "workout": "/v2/activity/workout", } for key, path in collections.items(): bundle["endpoints"][key] = fetch_collection( path=path, access_token=access_token, start=start, end=end, limit=args.limit, max_pages=args.max_pages, ) ``` The normalized output retains profile data: ```python prof = endpoints.get("profile_basic", {}).get("data") or {} meas = endpoints.get("body_measurement", {}).get("data") or {} ``` ```python "profile": { "name": prof.get("name") or prof.get("full_name"), "email": prof.get("email"), }, ``` However, the renderer outputs only recovery, sleep, cycle, and workout summary fields and does not use profile or body-measurement information. ### Technical Analysis The declared functionality includes multiple possible metric ...[truncated 1584 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Default to the smallest useful scope set, such as recovery, sleep, and cycles for a basic daily summary. 2. Add explicit command-line options selecting requested metrics and derive OAuth scopes from those selections. 3. Fetch profile, body measurements, and workouts only when the user explicitly requests those categories. 4. Explain each optional scope before authorization and obtain informed user consent. 5. Avoid storing response fields that are not used by normalization or rendering. 6. Support reauthorization when a later operation genuinely requires an additional scope rather than requesting all scopes in advance. 7. Document the relationship between each command, endpoint, and required OAuth scope. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (28)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
2) 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).

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
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
"post" : {
        "tags" : [ "Partner" ],
        "summary" : "Request a partner client token",
        "description" : "Exchanges partner client credentials for an access token.",
        "operationId" : "requestToken",
        "requestBody" : {
          "content" : {
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
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The skill advertises very broad trigger phrases such as 'pull WHOOP data' and 'push WHOOP updates to any chat channel', which can cause the integration to activate on ambiguous requests and process sensitive health data without sufficiently explicit user intent. In a health-data context, overbroad invocation increases the chance of accidental retrieval or sharing of private biometric information.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The description promotes local token storage and sending WHOOP updates to external chat channels, but it does not clearly warn users that OAuth tokens will be stored locally and that sensitive health data may be copied into third-party messaging platforms. Because WHOOP data includes personal wellness and body metrics, the absence of an upfront privacy warning materially increases the risk of unintended exposure.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This OpenAPI manifest documents endpoints that retrieve sensitive user data such as body measurements, email, sleep, recovery, and workout information, but the file does not include any warning or disclosure about handling personal or health-related data. For markdown and manifest-style descriptions, SQP-2 applies when behaviors affecting user privacy are described without corresponding warnings.

Missing User Warnings

Medium
Confidence
82% confidence
Finding
This markdown file documents OAuth authorization, token exchange, and endpoints for profile, body measurements, sleep, recovery, and workout data, all of which involve sensitive personal data. Under the markdown-file criteria for missing warnings, the description should explicitly warn that the skill may access health/profile data and requires secure handling of OAuth tokens and user consent.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code performs multiple HTTP requests to the WHOOP API for profile, body measurement, recovery, sleep, cycle, and workout data, then writes the collected raw bundle to the path supplied by the user. Although the module docstring describes the behavior, there is no runtime disclosure, confirmation, or explicit warning that sensitive health data will be retrieved and saved locally.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The code silently defaults to a specific locale/timezone, which is a natural-language policy concern because it imposes a regional setting without explicit user choice. The CLI allows --tz, but the default behavior still selects Asia/Shanghai automatically rather than prompting or using a neutral/default-local option.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code assembles and writes profile fields including name and email, along with health-related summary data, to the output file. Although the module docstring explains normalization, there is no confirmation prompt, print/log message, or explicit warning that sensitive user data will be persisted to disk.

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.

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.

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.