T09 · Insecure Skill Coding Practices
- Location
scripts/whoop_oauth.py:127- Finding
OAuth Tokens Stored Without Restrictive File Permissions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This Whoop skill is mostly a coherent health-data integration, but it handles OAuth credentials in ways users should review carefully before installing.
Install only if you are comfortable granting read access to sensitive Whoop health data and managing OAuth secrets yourself. Prefer using a local redirect flow or trusted secret store, restrict token/config file permissions, do not send codes or tokens through chat apps, and revoke the Whoop app authorization if a token was exposed.
scripts/whoop_oauth.py:127OAuth Tokens Stored Without Restrictive File Permissions
scripts/whoop_oauth.py:64OAuth State Is Generated but Never Validated
references/oauth-pages/redirect.html:69OAuth Callback Exposes Credentials Through URL, Clipboard, and Messaging Instructions
README.md:18Mutable Installation Artifact and Unpinned Dependency
The README emphasizes indefinite access via automatic refresh of access tokens using refresh tokens, but does not warn that possession of the refresh token effectively grants persistent account access until revoked. In the context of a health-data integration, stolen tokens could allow long-term unauthorized retrieval of sensitive wellness and workout information, making the omission more dangerous than in a low-sensitivity integration.
No more hourly re-authorization! The skill automatically refreshes your access token using the refresh token. Your token stays valid indefinitely.
If you ever need to manually refresh:
The declared purpose focuses on fetching and analyzing health metrics, but the documented behavior also includes OAuth app setup, authorization, token exchange, and local token persistence. This mismatch can mislead users and orchestrators about the sensitivity of the skill, causing it to be invoked in contexts where credential collection and authentication state management were not expected.
The redirect page explicitly displays OAuth authorization material, including access tokens, and tells the user to forward that secret to an assistant over Telegram/WhatsApp. OAuth codes and especially bearer tokens are credentials; exposing them in the browser UI and normalizing manual transfer over consumer messaging channels greatly increases the chance of credential theft, account takeover of the integration, and unauthorized API access. In this skill context, the page is handling health-related Whoop data, so compromise could expose sensitive personal wellness information in addition to API access.
Displaying an 'Access Token' field in the OAuth redirect page indicates that bearer credentials may be surfaced directly to the end user in the browser. Bearer tokens grant API access to protected resources without additional proof of possession, so exposing them in page content increases the risk of theft via shoulder surfing, screenshots, browser history/context, malicious extensions, or accidental sharing. In a health-data integration, misuse of such a token can lead to sensitive data disclosure.
</div>
<div id="authToken" style="display:none;">
<div class="label">Access Token:</div>
<div class="code-box" id="tokenDisplay"></div>
<button class="copy-btn" onclick="copyToken()">Copy Token</button>
</div>
The page instructs users to copy OAuth credentials and send them through Telegram/WhatsApp without any warning that these are sensitive secrets. This creates a direct social-engineering and exfiltration path for OAuth material, and any party with access to the message or clipboard can reuse the code/token to impersonate the user to the Whoop API. Because the skill processes personal health and training data, the resulting privacy impact is elevated.
Manually relaying OAuth codes or tokens to an assistant over consumer messaging channels bypasses standard trust boundaries and secure token handling. This pattern turns a controlled OAuth exchange into a human-mediated secret-sharing workflow, making interception, misdelivery, chat history retention, device compromise, and insider misuse realistic attack paths. Given the skill's access to Whoop account data, exploitation could expose private health metrics and enable unauthorized use of the integration.
The instructions explicitly guide the user to obtain an access token and store it for reuse, but do not describe safeguards for protecting that credential. Exposure of a reusable API token could allow an attacker to access sensitive health and activity data, and the skill context makes this more serious because the data includes recovery, sleep, HRV, and other personal biometric information.
1. Go to https://app.whoop.com/
2. Navigate to Settings → Developer
3. Create a new application
4. Complete OAuth flow to get access token
5. Store token in `~/.whoop_token` or set `WHOOP_API_TOKEN` environment variable
Token refresh is required when token expires (check API response for 401 errors).
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
raise ValueError(f"No Whoop API token found. Set WHOOP_API_TOKEN or create {self.token_file}")
def _refresh_token(self):
"""Refresh expired access token using refresh token"""
refresh_token_file = Path(self.token_file).parent / ".whoop_refresh_token"
if not refresh_token_file.exists():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
self.token = new_access_token
self.headers["Authorization"] = f"Bearer {new_access_token}"
print("✅ Access token auto-refreshed!", file=sys.stderr)
return True
else:
raise ValueError(f"Token refresh failed: {response.status_code} - {response.text}")
The script prints part of the received access token to stdout, which can leak sensitive credentials into terminal scrollback, shell logging, CI logs, remote session transcripts, or support screenshots. Even partial token disclosure is unnecessary and unsafe for secrets tied to private health data access.
expires_in = token_data.get("expires_in")
print("\n" + "=" * 70)
print("✅ SUCCESS! Access token received")
print("=" * 70)
print(f"\n🔑 Access Token:\n{access_token[:20]}...{access_token[-20:]}")
The script prints part of the refresh token, a long-lived credential that can be used to mint new access tokens and maintain persistent account access. Exposure through console logs or transcripts is especially dangerous because refresh tokens often outlive access tokens and enable ongoing access to sensitive health information.
print("\n" + "=" * 70)
print("✅ SUCCESS! Access token received")
print("=" * 70)
print(f"\n🔑 Access Token:\n{access_token[:20]}...{access_token[-20:]}")
if refresh_token:
print(f"\n🔄 Refresh Token:\n{refresh_token[:20]}...{refresh_token[-20:]}")
This line writes the access token to a plaintext file in the home directory without explicit permission hardening or secure secret storage. That creates a credential exposure risk to other local processes, users, backups, and malware, especially given the health-data scope of the Whoop account.
# Save both access and refresh tokens
DEFAULT_TOKEN_FILE.write_text(access_token)
print(f"\n💾 Access token saved to: {DEFAULT_TOKEN_FILE}")
if refresh_token:
refresh_file = DEFAULT_TOKEN_FILE.parent / ".whoop_refresh_token"
This line writes the refresh token to a plaintext file, enabling persistent reuse of the account if the file is later read or copied. Because refresh tokens support silent re-authentication, compromise here has greater persistence and impact than access-token leakage alone.
refresh_file = DEFAULT_TOKEN_FILE.parent / ".whoop_refresh_token"
refresh_file.write_text(refresh_token)
print(f"💾 Refresh token saved to: {refresh_file}")
print("\n✨ Refresh token will auto-renew your access token when it expires!")
print("\n" + "=" * 70)
print("🎉 SETUP COMPLETE!")
print("=" * 70)
The README instructs users to place OAuth client_id and client_secret into a local JSON file and later use long-lived refresh capability, but it provides no warning about securing that file, excluding it from version control, or protecting refresh/access tokens. In a skill that handles health-account API access, this omission can lead to credential leakage through shell history, backups, shared machines, or accidental repository commits, enabling unauthorized access to sensitive user data.
The skill describes capabilities that involve network access, reading configuration files, and writing tokens to disk, but it does not declare any explicit tool scope or permission boundaries. That increases the chance the host agent grants broader access than users expect, making credential handling and external API access less transparent and harder to constrain.
The activation text is broad enough to match generic health, training, briefing, trend, or alert requests, not just clearly Whoop-specific ones. In a multi-skill system this can cause over-triggering, leading the agent to request or use sensitive fitness data when the user did not intend to invoke this integration.
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.
- Python 3.7+
- `requests` library: `pip install requests`
**Step 1: Create Whoop Developer App**
1. Go to https://app.whoop.com/ → Settings → Developer
2. Click "Create Application"
The documentation instructs users to place a client secret in a local JSON file and states that tokens are saved to ~/.whoop_token, but it does not warn that these are sensitive secrets or recommend file permission hardening. If stored insecurely, local users, backups, logs, or other processes could read reusable OAuth credentials and access private health data.
The README documents a redirect page that extracts an OAuth authorization code from the URL and automatically copies it to the clipboard. Authorization codes are sensitive short-lived credentials; automatically placing them in the clipboard increases the chance of accidental disclosure to other applications, browser extensions, shared clipboard services, or user mishandling, especially since the README provides no warning about the sensitivity of the code.
The script automatically copies the authorization code or token into the system clipboard after page load, without a clear consent step. Clipboard contents are broadly accessible to the user and potentially to other applications or browser contexts, so silently placing secrets there increases the risk of unintended disclosure and accidental pasting into chats, logs, or other unsafe destinations. In combination with the page's instruction to message the secret, this makes exfiltration even easier.
No suspicious patterns detected.