Back to skill

Security audit

simplisafe-mcp

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherently aimed at SimpliSafe control, but it handles powerful home-security access with broad shell/API authority and a weak access-token cache.

Review this before installing if you are comfortable letting an agent operate your SimpliSafe account. Use it only in a trusted local account, confirm every arm/disarm or lock/unlock action, avoid requesting PINs unless necessary, verify the external bootstrap-auth.mjs source, and consider hardening or disabling the $TMPDIR token cache.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
references/ss-helpers.sh:23
Finding
Predictable Shared Token Cache Permits Symlink-Based File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `references/ss-helpers.sh`, lines 23 and 53–87 **Vulnerability Type**: Predictable unsafe temporary file and symlink following **Risk Level**: Medium ### Vulnerable Code ```bash SS_TOKEN_CACHE="${TMPDIR:-/tmp}/simplisafe-skill-token.json" ``` ```bash ss_access_token() { local now now=$(date +%s) if [ -f "$SS_TOKEN_CACHE" ]; then local cached_exp cached_tok cached_exp=$(jq -r '.expiresAt // 0' "$SS_TOKEN_CACHE" 2>/dev/null || echo 0) cached_tok=$(jq -r '.accessToken // ""' "$SS_TOKEN_CACHE" 2>/dev/null || echo '') # 10# forces base-10 so a value with a leading zero can't be read as octal, # which is a real bash failure mode (zsh has no such rule, so an untested # helper works for its author and breaks for everyone else). if [ -n "$cached_tok" ] && [ "$((10#${cached_exp:-0}))" -gt "$((now + 120))" ]; then printf '%s' "$cached_tok" return 0 fi fi local rt rt=$(ss_refresh_token) || return 1 local body response access expires # Build the JSON with jq so a token containing quotes/backslashes can't break it. body=$(jq -nc --arg cid "$SS_CLIENT_ID" --arg rt "$rt" \ '{grant_type:"refresh_token", client_id:$cid, refresh_token:$rt}') response=$(curl -sS -X POST "$SS_AUTH_URL" -H 'Content-Type: application/json' -d "$body") || return 1 access=$(printf '%s' "$response" | jq -r '.access_token // ""') if [ -z "$access" ]; then echo "ss_access_token: refresh failed." >&2 # Echo only the error fields, never the whole body. printf '%s' "$response" | jq -r '" \(.error // "?"): \(.error_description // "?")"' >&2 echo " If the token was revoked, re-run scripts/bootstrap-auth.mjs." >&2 return 1 fi expires=$(printf '%s' "$response" | jq -r '.expires_in // 3600') ( umask 077; jq -nc --arg t "$access" --argjson e "$((now + expires))" \ '{accessToken:$t, expiresAt:$e}' > "$SS_TOKEN_CACHE" ) printf '%s' "$access" } ``` ...[truncated 2694 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the cache in a private per-user runtime directory rather than directly under a shared `/tmp` namespace. Prefer `${XDG_RUNTIME_DIR}` after validating that it is owned by the current user and inaccessible to other users. 2. If a runtime directory is unavailable, create a private directory with `mktemp -d` and mode `0700`. 3. Use a per-user cache name or directory so different users cannot share or interfere with the same path. 4. Reject cache paths that are symbolic links or are not regular files owned by the current user. 5. Write tokens atomically: - Create a temporary file inside the private directory with exclusive creation. - Set mode `0600`. - Write and validate the JSON. - Atomically rename it over the cache file. 6. Validate the ownership and permissions of an existing cache before reading it. 7. Consider avoiding persistent access-token caching entirely if the operational cost of obtaining a fresh token is acceptable. A hardened design should resemble: ```bash cache_root="${XDG_RUNTIME_DIR:-}" if [ -z "$cache_root" ] || [ ! -d "$cache_root" ] || [ ! -O "$cache_root" ]; then cache_root=$(mktemp -d "${TMPDIR:-/tmp}/simplisafe-${UID}.XXXXXX") || return 1 chmod 700 "$cache_root" || return 1 fi SS_TOKEN_CACHE="$cache_root/simplisafe-token.json" tmp_cache=$(mktemp "$cache_root/token.XXXXXX") || return 1 chmod 600 "$tmp_cache" || { rm -f "$tmp_cache" return 1 } jq -nc --arg t "$access" --argjson e "$((now + expires))" \ '{accessToken:$t, expiresAt:$e}' > "$tmp_cache" || { rm -f "$tmp_cache" return 1 } mv -f "$tmp_cache" "$SS_TOKEN_CACHE" ``` Ownership, file-type, and permission checks must still be applied before consuming an existing cache. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (7)

Credential Access

High
Category
Privilege Escalation
Content
`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,
and on a non-2xx prints to **stderr** and returns 1 — so a failure never looks
like an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted
only when stale; the helpers never write to the repo's `.env`.

## Resolve first
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
#   not rotate refresh tokens, so there is nothing to write back, and the MCP
#   server's own state is never touched.
#
#   The minted access token is cached in this skill's OWN state file under
#   TMPDIR (mode 0600), never in the repo's .env.

SS_CLIENT_ID='42aBZ5lYrVW12jfOuu3CQROitwxg9sN5'
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
#   not rotate refresh tokens, so there is nothing to write back, and the MCP
#   server's own state is never touched.
#
#   The minted access token is cached in this skill's OWN state file under
#   TMPDIR (mode 0600), never in the repo's .env.

SS_CLIENT_ID='42aBZ5lYrVW12jfOuu3CQROitwxg9sN5'
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
}

# Print a valid access token, minting one only when the cache is cold or stale.
# SimpliSafe access tokens last 3600s; refresh 120s early to avoid a race.
ss_access_token() {
  local now
  now=$(date +%s)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill clearly instructs use of shell commands (`curl`, `jq`, `source`, `node`) but does not declare any tool scope or allowed-tools restrictions. That creates an authorization and transparency gap: an agent may be able to execute shell-capable actions without the skill metadata explicitly constraining or signaling that capability.

External Transmission

Medium
Category
Data Exfiltration
Content
SS_CLIENT_ID='42aBZ5lYrVW12jfOuu3CQROitwxg9sN5'
SS_AUTH_URL='https://auth.simplisafe.com/oauth/token'
SS_API_BASE='https://api.simplisafe.com/v1'
SS_TOKEN_CACHE="${TMPDIR:-/tmp}/simplisafe-skill-token.json"

# Resolve the refresh token. Prints it, or fails with an actionable message.
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
body=$(jq -nc --arg cid "$SS_CLIENT_ID" --arg rt "$rt" \
    '{grant_type:"refresh_token", client_id:$cid, refresh_token:$rt}')

  response=$(curl -sS -X POST "$SS_AUTH_URL" -H 'Content-Type: application/json' -d "$body") || return 1
  access=$(printf '%s' "$response" | jq -r '.access_token // ""')

  if [ -z "$access" ]; then
Confidence
70% 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.