T09 · Insecure Skill Coding Practices
- Location
scripts/webhook_receiver.py:157- Finding
Unauthenticated webhook permits forged meeting-data injection
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This skill performs meeting-data work, but it needs Review because it stores highly sensitive meeting and lifelog records with weak controls.
Install only if you are comfortable storing meeting transcripts, participant details, webhook payloads, and Limitless lifelogs under ~/.readai. Do not expose the webhook endpoint publicly without adding signature or token validation, HTTPS, rate limits, and request-size limits. Use a restrictive umask or private directory/file permissions, and avoid --ai unless sending conversation excerpts through the Claude CLI is acceptable for your data.
scripts/webhook_receiver.py:157Unauthenticated webhook permits forged meeting-data injection
scripts/readai_client.py:136Sensitive meeting and lifelog data is stored without enforced restrictive permissions
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{API_BASE}/lifelogs", headers=headers, params=params, timeout=30)
resp.raise_for_status()
data = resp.json()
The skill advertises broad Read AI fetching, search, digest, analytics, and webhook functionality, but the documented implementation appears narrower and stores webhook payloads and source IPs locally. This discrepancy is dangerous because users may not realize the skill persists inbound meeting data and network metadata, creating privacy and compliance exposure.
The skill advertises broad Read AI fetching, search, digest, analytics, and webhook functionality, but the documented implementation appears narrower and stores webhook payloads and source IPs locally. This discrepancy is dangerous because users may not realize the skill persists inbound meeting data and network metadata, creating privacy and compliance exposure.
The skill advertises broad Read AI fetching, search, digest, analytics, and webhook functionality, but the documented implementation appears narrower and stores webhook payloads and source IPs locally. This discrepancy is dangerous because users may not realize the skill persists inbound meeting data and network metadata, creating privacy and compliance exposure.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
chmod 600 ~/.config/readai/api-key
For Limitless: Get key from limitless.ai dashboard.
For Read AI API: Get key from Read AI Dashboard > Settings > Integrations > API Keys.
## Quick Commands
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
chmod 600 ~/.config/readai/api-key
For Limitless: Get key from limitless.ai dashboard.
For Read AI API: Get key from Read AI Dashboard > Settings > Integrations > API Keys.
## Quick Commands
The skill manifest describes Read AI meeting functionality, but this script actually accesses Limitless pendant lifelog data using separate credentials and stores that data locally. This mismatch expands the skill's effective data access beyond user expectations and can cause unauthorized collection or handling of a different, highly sensitive data source.
The skill declares broad capabilities to read/write local files, access environment-stored credentials, invoke shell commands, and communicate over the network, but it does not constrain or disclose these powers with an explicit tool scope. That makes it easier for the skill to overreach its stated purpose and harder for users or reviewers to understand what data and system access it may exercise.
This skill handles highly sensitive meeting artifacts, including transcripts, summaries, action items, and webhook-delivered conversation data, yet it provides no privacy or data-handling warning. In this context, omission is risky because users may expose confidential business, legal, HR, or personal discussions without understanding storage, retention, or sharing implications.
The skill instructs users to persist an API key in a fixed path under the home directory, creating durable credential storage outside a managed secret store. While common, this increases risk from local compromise, accidental backup/sync leakage, and ambiguous lifecycle management of a key that can unlock sensitive meeting and lifelog data.
# One-time setup
mkdir -p ~/.config/readai
echo "YOUR_API_KEY" > ~/.config/readai/api-key
chmod 600 ~/.config/readai/api-key
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
mkdir -p ~/.config/readai echo "YOUR_API_KEY" > ~/.config/readai/api-key chmod 600 ~/.config/readai/api-key
For Limitless: Get key from limitless.ai dashboard.
This markdown file documents webhook payloads containing personal and potentially sensitive meeting content, including participant email addresses and full transcript text. Under the markdown-file criteria for SQP-2, the description should warn users about privacy and data-handling implications, but no such warning appears in the webhook section or setup steps.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
TIMEZONE = os.environ.get("READAI_TIMEZONE", "America/Chicago")
TZ = ZoneInfo(TIMEZONE)
API_BASE = "https://api.limitless.ai/v1"
API_KEY_PATH = os.path.expanduser("~/.config/readai/api-key")
DATA_DIR = Path(os.path.expanduser("~/.readai/lifelogs"))
PAGE_LIMIT = 10
The code invokes an external Claude CLI to process meeting/lifelog content even though that capability is not justified by the declared skill scope. Hidden secondary processing of sensitive transcripts increases privacy and compliance risk because users may believe data is only being fetched from the advertised source, not forwarded elsewhere for inference.
The AI summary feature transmits highly sensitive conversation content, participant names, and action items to an external AI tool without any explicit privacy notice or consent flow. In a meeting-data skill, this is especially dangerous because transcripts may contain confidential business, personal, legal, or regulated information, and users are not warned that it will be shared for summarization.
The script sends meeting/lifelog content to an external local CLI tool (claude) for summarization. While the subprocess invocation itself is not shell-injection prone because it uses an argument list, it still causes sensitive transcript data to leave the script's primary trust boundary and be processed by another tool/service, which is risky for private meeting data.
Keep it concise and actionable."""
try:
result = subprocess.run(
["claude", "-p", "--model", "sonnet"],
input=prompt, capture_output=True, text=True, timeout=120,
)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# ---------------------------------------------------------------------------
TIMEZONE = "America/Chicago"
TZ = ZoneInfo(TIMEZONE)
API_BASE = "https://api.read.ai/v1"
API_KEY_PATH = os.path.expanduser("~/.config/readai/api-key")
DATA_DIR = Path(os.path.expanduser("~/.readai/meetings"))
INDEX_PATH = DATA_DIR / "index.json"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
# ---------------------------------------------------------------------------
TIMEZONE = "America/Chicago"
TZ = ZoneInfo(TIMEZONE)
API_BASE = "https://api.read.ai/v1"
API_KEY_PATH = os.path.expanduser("~/.config/readai/api-key")
DATA_DIR = Path(os.path.expanduser("~/.readai/meetings"))
INDEX_PATH = DATA_DIR / "index.json"
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.
p = Path(API_KEY_PATH)
if not p.exists():
print(f"Error: API key not found at {API_KEY_PATH}", file=sys.stderr)
print("Set up: mkdir -p ~/.config/readai && echo 'YOUR_KEY' > ~/.config/readai/api-key", file=sys.stderr)
sys.exit(1)
return p.read_text().strip()
The client persistently caches meeting metadata and full meeting contents, potentially including transcripts, participant identities, summaries, and action items, to local disk without clear disclosure in the described skill behavior. This creates a confidentiality risk because highly sensitive meeting records may remain on disk longer than users expect and could be accessed by other local users, backup systems, or later processes.
The code establishes local storage locations for cached meeting data and transcripts without any user-facing warning that sensitive conversational data will be written to disk. For meeting software, this is particularly risky because transcripts and participant details commonly include confidential business or personal information.
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
if include_transcript:
params["include"] = "transcript"
resp = requests.get(f"{API_BASE}/meetings/{meeting_id}", headers=headers, params=params, timeout=30)
resp.raise_for_status()
meeting = resp.json()
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
def get_meeting_transcript(api_key: str, meeting_id: str) -> list[dict]:
"""Get the full transcript for a meeting."""
headers = api_headers(api_key)
resp = requests.get(f"{API_BASE}/meetings/{meeting_id}/transcript", headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
return data.get("transcript", data.get("entries", data.get("data", [])))
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
def get_meeting_action_items(api_key: str, meeting_id: str) -> list[dict]:
"""Get action items for a meeting."""
headers = api_headers(api_key)
resp = requests.get(f"{API_BASE}/meetings/{meeting_id}/action-items", headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
return data.get("action_items", data.get("actionItems", data.get("data", [])))
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
def get_meeting_summary(api_key: str, meeting_id: str) -> dict:
"""Get AI summary for a meeting."""
headers = api_headers(api_key)
resp = requests.get(f"{API_BASE}/meetings/{meeting_id}/summary", headers=headers, timeout=30)
resp.raise_for_status()
return resp.json()