T09 · Insecure Skill Coding Practices
- Location
scripts/oura_api.py:19- Finding
Configurable API origin can expose the Oura bearer token
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill matches its Oura health-data purpose, but it can send the user's Oura bearer token to an arbitrary configured API URL while claiming it only uses the official Oura API.
Review before installing. Only use this skill if you are comfortable giving it access to sensitive Oura health and profile data. Keep the credentials file private, rotate the token if exposed, and remove or hard-code the base_url to https://api.ouraring.com/v2 before use, especially before running the alert checker from cron or another heartbeat system.
scripts/oura_api.py:19Configurable API origin can expose the Oura bearer token
scripts/health_alerts.py:15Health alert checker sends the bearer token to an unrestricted configured origin
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
## Setup
### 1. Get a Personal Access Token
1. Go to [cloud.ouraring.com](https://cloud.ouraring.com/personal-access-tokens)
2. Sign in with your Oura account
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
## Setup
### 1. Get a Personal Access Token
1. Go to [cloud.ouraring.com](https://cloud.ouraring.com/personal-access-tokens)
2. Sign in with your Oura account
The README instructs the user to store a long-lived personal access token in a local plaintext credentials.json file. Even with 600 permissions, plaintext bearer tokens on disk can be stolen by local malware, backup leakage, shell-history mistakes, or accidental file disclosure, granting access to sensitive health data.
mkdir -p ~/.config/oura
cat > ~/.config/oura/credentials.json << 'EOF'
{
"personal_access_token": "YOUR_TOKEN_HERE",
"base_url": "https://api.ouraring.com/v2"
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
# Oura Ring Skill
Query Oura Ring health data via the Oura API v2. Requires a personal access token at `~/.config/oura/credentials.json`.
## Setup
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
# Oura Ring Skill
Query Oura Ring health data via the Oura API v2. Requires a personal access token at `~/.config/oura/credentials.json`.
## Setup
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
# Oura Ring Skill
Query Oura Ring health data via the Oura API v2. Requires a personal access token at `~/.config/oura/credentials.json`.
## Setup
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
# Oura Ring Skill
Query Oura Ring health data via the Oura API v2. Requires a personal access token at `~/.config/oura/credentials.json`.
## Setup
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
import urllib.parse
from datetime import datetime, timedelta
CREDS_PATH = os.path.expanduser("~/.config/oura/credentials.json")
def load_credentials():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
import urllib.parse
from datetime import datetime, timedelta
CREDS_PATH = os.path.expanduser("~/.config/oura/credentials.json")
def load_credentials():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
import urllib.parse
from datetime import datetime, timedelta
CREDS_PATH = os.path.expanduser("~/.config/oura/credentials.json")
def load_credentials():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
import urllib.parse
from datetime import datetime, timedelta
CREDS_PATH = os.path.expanduser("~/.config/oura/credentials.json")
def load_credentials():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
import urllib.parse
from datetime import datetime, timedelta
CREDS_PATH = os.path.expanduser("~/.config/oura/credentials.json")
def load_credentials():
The setup encourages creation of a persistent personal access token for ongoing use. Long-lived session material increases the blast radius of local compromise because an attacker who obtains the token may access historical and current health data until the token is revoked or rotated.
1. Go to [cloud.ouraring.com](https://cloud.ouraring.com/personal-access-tokens)
2. Sign in with your Oura account
3. Click **Create New Personal Access Token**
4. Copy the token
### 2. Save Credentials
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
"base_url": "https://api.ouraring.com/v2" } EOF chmod 600 ~/.config/oura/credentials.json
### 3. Verify
The skill documents network access to the Oura API but does not declare any explicit tool scope or permissions boundary. This creates a transparency and governance gap: users and the platform cannot easily see that the skill will transmit data off-device, which is especially important for health data.
The trigger list includes broad phrases such as 'heart rate', 'readiness', 'oura', and 'health briefing', which can match normal conversation and invoke the skill unexpectedly. Because the skill accesses sensitive health information, unintended invocation can expose private data or cause queries to be sent without clear user intent.
The description explains capabilities but does not prominently warn that the skill accesses highly sensitive personal health and profile data, including age, email, biological sex, sleep, HRV, and readiness metrics. Without a clear warning and consent model, users may disclose or retrieve protected personal information without understanding the privacy implications.
The script reads a personal access token from a credentials file under the user's home directory, which is sensitive credential access. Although the code comment explains silent exit behavior when credentials are missing, it does not disclose to the user that credentials will be read and used for authenticated API access.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
with open(CREDS_PATH) as f:
creds = json.load(f)
token = creds.get("personal_access_token")
base = creds.get("base_url", "https://api.ouraring.com/v2").rstrip("/")
if not token:
return None, None
return token, base
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
with open(CREDS_PATH) as f:
creds = json.load(f)
token = creds.get("personal_access_token")
base = creds.get("base_url", "https://api.ouraring.com/v2").rstrip("/")
if not token:
return None, None
return token, base
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
with open(CREDS_PATH) as f:
creds = json.load(f)
token = creds.get("personal_access_token")
base = creds.get("base_url", "https://api.ouraring.com/v2").rstrip("/")
if not token:
return None, None
return token, base
This code sends authenticated requests to the Oura API using a personal access token and retrieves sensitive health data, but the operation has no confirmation prompt, no logging/print disclosure, and no inline warning beyond a brief module docstring. For a code file handling privacy-sensitive network activity, the absence of any visible user disclosure matches the missing-warning criteria.
The client allows the API base URL to be overridden from a local credentials file, then attaches the bearer token to every request. If that file is modified to point to an attacker-controlled host, the script will transmit the personal access token and potentially health-query metadata to that host, enabling credential theft and unauthorized access to the user's Oura data.
with open(CREDS_PATH) as f:
creds = json.load(f)
token = creds.get("personal_access_token")
base = creds.get("base_url", "https://api.ouraring.com/v2").rstrip("/")
if not token:
die("No personal_access_token in credentials file")
return token, base
The example credentials format instructs users to store a configurable base_url alongside the personal access token, reinforcing the redirectable endpoint design. In the context of a health-data skill, this increases the chance that sensitive bearer tokens and downstream requests are sent to a non-Oura server if the config is tampered with or misconfigured.
return token, base
except FileNotFoundError:
die(f"Credentials file not found: {CREDS_PATH}\n"
f"Create it with: {{\"personal_access_token\": \"...\", \"base_url\": \"https://api.ouraring.com/v2\"}}")
except json.JSONDecodeError:
die(f"Invalid JSON in {CREDS_PATH}")
The script performs authenticated HTTP requests to the Oura API and retrieves personal and health-related information, including email, age, sex, sleep, heart rate, and readiness data. While network access is central to the tool's purpose, the file lacks any explicit user-facing warning, comment, or help text disclosing that sensitive personal data will be sent to and fetched from a third-party service.
No suspicious patterns detected.