Back to skill

Security audit

Spotify Intelligence

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches a Spotify assistant, but it asks for broad Spotify authority and stores sensitive listening and token data with under-scoped safeguards.

Install only if you are comfortable granting Spotify playback, playlist, and library permissions and storing Spotify tokens plus listening/profile data locally. Before use, restrict file permissions on data/tokens.json and the data directory, keep config.json trusted and unchanged, consider removing unused Spotify write scopes, and avoid enabling phone-context or relationship-status features unless you explicitly want that sensitive data used for recommendations.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth/oauth_auth.py:14
Finding
OAuth Tokens Are Stored Without Enforced Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth/oauth_auth.py:14-17`; `scripts/playback/playback_control.py:22-25` **Vulnerability Type**: Insecure plaintext credential storage and file permissions **Risk Level**: Medium ### Vulnerable Code `scripts/auth/oauth_auth.py:14-17`: ```python def save_tokens(tok): os.makedirs(os.path.dirname(TOK_PATH), exist_ok=True) with open(TOK_PATH, "w", encoding="utf-8") as f: json.dump(tok, f) ``` `scripts/playback/playback_control.py:22-25`: ```python def write_tokens(tok): os.makedirs(os.path.dirname(TOK_PATH), exist_ok=True) with open(TOK_PATH, "w", encoding="utf-8") as f: json.dump(tok, f) ``` ### Technical Analysis The Skill stores Spotify access and refresh tokens in plaintext at `data/tokens.json`. Both the initial token save and subsequent refresh-token rewrite use the process's default file-creation permissions. The code does not: - Create the file with an explicit owner-only mode such as `0600`. - Verify the file owner or current permissions. - Reject symbolic links. - Repair an existing file with unsafe permissions. - Use an operating-system credential store. The effective permissions therefore depend on the host's umask, ACLs, and any pre-existing file. This also conflicts with `references/config.md:22`, which states that the implementation should warn when token-file permissions are too broad. ### Attack Path 1. The Skill runs on a shared host or under an environment with a permissive umask or ACL. 2. OAuth authentication or token refresh writes `data/tokens.json` with permissions readable by another local account. 3. A local attacker reads the file and obtains the access token and refresh token. 4. The attacker uses the access token directly or exchanges the refresh token for new access tokens. 5. The attacker invokes Spotify APIs with the permissions granted during authorization. If the host already contains an attacker-controlled symbolic link at the ...[truncated 716 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create token files atomically with owner-only permissions: ```python import os import tempfile def save_tokens(tok): token_dir = os.path.dirname(TOK_PATH) os.makedirs(token_dir, mode=0o700, exist_ok=True) fd, temporary_path = tempfile.mkstemp(dir=token_dir) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as stream: json.dump(tok, stream) stream.flush() os.fsync(stream.fileno()) os.replace(temporary_path, TOK_PATH) os.chmod(TOK_PATH, 0o600) except Exception: try: os.unlink(temporary_path) except OSError: pass raise ``` 2. Check that the target and parent directory are owned by the current user. 3. Reject a token path that is a symbolic link or resolves outside the intended data directory. 4. On startup, warn or fail if the token file is readable or writable by group or other users. 5. Apply equivalent access-control checks on platforms that use ACLs rather than POSIX modes. 6. Prefer an operating-system credential manager or secret vault for refresh-token storage. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/playback/playback_control.py:52
Finding
Mutable API Base URL Can Redirect Spotify Bearer Tokens to an Untrusted Origin<![CDATA[ ## Vulnerability Details **File Location**: `scripts/playback/playback_control.py:52-60, 141` **Vulnerability Type**: Unvalidated authenticated request destination **Risk Level**: High ### Vulnerable Code `scripts/playback/playback_control.py:52-60`: ```python def api(method, url, token, body=None): req = urllib.request.Request(url, method=method) req.add_header("Authorization", f"Bearer {token}") if body is not None: raw = json.dumps(body).encode() req.add_header("Content-Type", "application/json") req.data = raw try: with urllib.request.urlopen(req, timeout=30) as r: ``` `scripts/playback/playback_control.py:139-141`: ```python tok = read_tokens() token = refresh_if_needed(cfg, tok) base = cfg["spotify"]["apiBase"] ``` Subsequent requests construct their destination from this value, for example: ```python data = api("GET", f"{base}/me/player/devices", token) ``` ### Technical Analysis The generic `api()` helper unconditionally attaches the Spotify bearer token to its supplied URL. The destination base is loaded from the mutable `config.json` file without validating: - That the scheme is HTTPS. - That the hostname is exactly `api.spotify.com`. - That no username, alternate port, or deceptive hostname is present. - That redirects remain on the approved Spotify origin. The distributed configuration currently contains the legitimate value `https://api.spotify.com/v1`; therefore, no active credential exfiltration destination is embedded in the reviewed package. However, configuration tampering or unsafe deployment customization can turn normal playback commands into authenticated requests to another server. The flagged network transmission is necessary for Spotify playback functionality, but allowing the credential-bearing destination to be changed without an origin allowlist exceeds the minimum safe privilege boundary. ### Attack Path 1. An attacker, compromised deployment process, or unsafe a ...[truncated 1362 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `apiBase` from mutable configuration unless alternate Spotify environments are genuinely required. 2. Hardcode the approved base URL: ```python SPOTIFY_API_BASE = "https://api.spotify.com/v1" ``` 3. Before attaching an authorization header, parse and validate every destination: ```python from urllib.parse import urlsplit def validate_spotify_url(url): parsed = urlsplit(url) if parsed.scheme != "https": raise RuntimeError("Spotify API requests must use HTTPS") if parsed.hostname != "api.spotify.com": raise RuntimeError("Unapproved Spotify API hostname") if parsed.port not in (None, 443): raise RuntimeError("Unapproved Spotify API port") if parsed.username or parsed.password: raise RuntimeError("Credentials in request URLs are prohibited") ``` 4. Validate the final request destination inside `api()`, not only when configuration is loaded. 5. Disable redirects or implement a redirect handler that rejects any scheme, hostname, or port change. 6. Protect `config.json` and the project directory against modification by untrusted local users. 7. Add tests proving that HTTP, deceptive subdomains, embedded credentials, nonstandard ports, and cross-origin redirects are rejected before the bearer token is sent. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth/oauth_auth.py:43
Finding
OAuth Authorization Code Is Accepted Without State Verification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth/oauth_auth.py:43-47` **Vulnerability Type**: OAuth login CSRF and authorization-session confusion **Risk Level**: Medium ### Vulnerable Code ```python cb = urllib.parse.urlparse(args.callback_url) qs = urllib.parse.parse_qs(cb.query) code = (qs.get("code") or [""])[0] if not code: raise SystemExit("callback-url missing code") ``` The extracted code is subsequently exchanged without checking a session-bound state value: ```python tok = exchange(code, redirect_uri, cid, sec) ``` ### Technical Analysis OAuth Authorization Code Flow requires the client to correlate the callback with the authorization request that it initiated. This is normally implemented using a cryptographically random `state` parameter. The supplied callback processor only extracts `code`. It does not: - Generate or persist an expected `state`. - Extract `state` from the callback. - Compare it using a timing-safe operation. - Expire state after one use. - Validate that the callback URL's scheme, host, port, and path match the configured redirect URI. Without state correlation, the script cannot determine whether the supplied callback belongs to the current user's authorization session. Exploitability depends on how the callback URL is produced and delivered because the authorization-initiation wrapper described in the documentation is not included in this project. Nevertheless, this processor accepts any exchangeable authorization code presented through its command-line argument. ### Attack Path A possible login-CSRF or account-confusion sequence is: 1. An attacker initiates Spotify authorization using the same client application and redirect URI. 2. The attacker authorizes an account and obtains or causes generation of a callback URL containing an authorization code. 3. Through social engineering or automation around the documented workflow, the attacker causes the victim to pass that callback URL to `oauth_auth. ...[truncated 1034 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically random state value before opening the Spotify authorization URL: ```python import secrets state = secrets.token_urlsafe(32) ``` 2. Store the expected state in an owner-only temporary file or another session-bound local store. 3. Include the state in the authorization request. 4. Require the callback to contain exactly one `state` value and compare it to the expected value with `secrets.compare_digest`. 5. Delete or invalidate state immediately after successful validation and enforce a short expiration period. 6. Reject callbacks whose scheme, hostname, port, or path do not exactly match the configured redirect URI. 7. Reject callbacks containing OAuth `error` parameters and provide explicit error handling. 8. Consider adding PKCE to bind the authorization code to the initiating client session. 9. Add tests for missing, duplicated, expired, mismatched, and replayed state values. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (39)

Credential Access

High
Category
Privilege Escalation
Content
data/*.sqlite
data/*.db
data/tokens.json
.env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose focuses on Spotify auth, playback, recommendations, and governance, but the detected behavior includes local telemetry/metrics logging and generic request instrumentation not clearly disclosed in the description. Undisclosed persistence and instrumentation can hide secondary data collection behavior, undermining user trust and making review of data handling and privacy exposure more difficult.

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding
The declared purpose focuses on Spotify auth, playback, recommendations, and governance, but the detected behavior includes local telemetry/metrics logging and generic request instrumentation not clearly disclosed in the description. Undisclosed persistence and instrumentation can hide secondary data collection behavior, undermining user trust and making review of data handling and privacy exposure more difficult.

Context-Inappropriate Capability

High
Confidence
96% confidence
Finding
This logic directly uses a sensitive relationship-status flag to change how tracks tagged with an emotional condition are classified and archived. That is risky because it operationalizes intimate personal data for automated behavioral decisions, which is disproportionate to the stated music-intelligence purpose and could cause privacy, compliance, and user-trust harm if the data is wrong, leaked, or repurposed.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill advertises Python entrypoints with access to environment variables, local file read/write, network, and shell-like execution surfaces, but the manifest declares no explicit tool scope or permission boundaries. This weakens least-privilege controls and makes it easier for a caller or downstream runner to invoke broader capabilities than users would reasonably infer from the metadata.

External Transmission

Medium
Category
Data Exfiltration
Content
"playlist-modify-public",
      "user-top-read"
    ],
    "apiBase": "https://api.spotify.com/v1"
  },
  "storage": {
    "dbPath": "./data/spotify-intelligence.sqlite",
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The document’s instructional text and operational descriptions are written entirely in German, including user-facing workflow steps and rationale examples. There is no indication that German is optional, user-selected, or required for a region-specific compliance reason, which creates a natural-language locale policy concern.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The documentation expands the skill from Spotify-focused functionality into phone-node signal ingestion, including GPS, Bluetooth, and motion context, which is a meaningful scope increase beyond the stated purpose. In a music recommendation skill, collecting environmental/device telemetry creates unnecessary privacy and trust risk because operators may enable broader sensing without clear product justification or consent boundaries.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The documentation describes enabling and ingesting sensitive phone-node context signals with no accompanying privacy notice, consent flow, or warning about the implications of collecting location, Bluetooth proximity, and motion data. In the context of a consumer-facing Spotify skill, this is especially risky because users would not reasonably expect this level of telemetry, making covert or accidental privacy invasion more likely.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The file instructs ingestion and use of Bluetooth, location, and motion data without clearly demonstrating necessity for the declared Spotify intelligence purpose. That mismatch increases the chance of overcollection and secondary use of sensitive contextual data, especially since location and device-nearby signals can reveal habits, presence, and routines.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The file's instructional content is written in German throughout, without offering a language choice or explaining that the skill is intentionally region- or locale-specific. Under the policy, forcing a specific language without opt-in is a natural-language policy violation.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
This read-layer document includes explicit write-capable restore workflows against the Spotify API, which weakens the boundary implied by a read-focused component. That mismatch can cause operators or downstream automation to invoke state-changing actions from a supposedly read-only context, increasing the risk of unintended playlist modification or privilege overreach.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The documentation instructs continuous playback capture and storage of listening behavior without an explicit privacy warning, consent guidance, retention limits, or minimization controls. In a skill that profiles Spotify activity, this can lead to silent collection of sensitive behavioral data such as habits, preferences, and presence patterns.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The documentation repeatedly instructs users to run PowerShell with `-ExecutionPolicy Bypass`, which weakens a built-in safety control and normalizes unsafe execution practices. In a skill context where users may copy-paste commands verbatim, this increases the chance of executing tampered or untrusted scripts without policy-based friction or review.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill persists access and refresh tokens to data/tokens.json, which is a sensitive file write involving credentials. There is no confirmation prompt, warning comment/docstring, or prior user-facing disclosure before saving these tokens.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The code reads SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET from environment variables and uses them in an HTTP token exchange request. Although this is functionally expected for OAuth, the file provides no user-facing warning, explanatory comment, or visible disclosure about handling and transmitting these credentials.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The user-facing question string is written only in German and there is no visible mechanism in this file for language selection, opt-in, or locale-aware behavior. This creates a natural-language policy issue because the skill imposes a specific language on users without offering a choice or documenting a justified regional restriction.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The schema and default rules introduce storage and use of life-phase and emotionally sensitive labels such as relationship status and heartbreak, which goes beyond ordinary playlist management and creates sensitive profiling risk. In a Spotify playlist skill, collecting and acting on intimate personal context is not necessary for core functionality and can expose users to privacy harm if accessed, inferred, or reused unexpectedly.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The default rules embed German-language playlist names such as "Löschvorschläge" and "Emotions-Archiv" directly in the skill logic. This imposes a specific locale in behavior without any user opt-in or documented region-specific justification, which matches the language/locale policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The script emits a user-facing question string in German only: "Soll ich mal aufräumen?...". This is a natural-language locale policy issue because the skill does not offer any language choice, opt-in, or documented region-specific justification.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
This script is in a read-side path but performs persistent database writes by inserting raw event records and updating aggregate statistics. That violates read/write separation and can make seemingly low-risk read operations mutate state, enabling data poisoning, unintended side effects, and easier abuse by any caller that is only expected to have observational access.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
This file lives under scripts/read and is named rebuild-cleanup-candidates, which suggests a read/rebuild-style reporting operation, but the code creates tables, alters schema, inserts system metadata, writes playlist_cleanup_candidates rows, and updates last-run state. That behavior is materially write-oriented rather than purely read/analysis, creating a semantic mismatch with the apparent documented intent implied by the file placement and naming.

Context Window Stuffing

Medium
Category
Memory Poisoning
Content
last_seen_at, oldest_playlist_added_at, years_in_playlist, playlist_count,
                  total_played_ms, total_played_minutes,
                  score, reason
                ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                """,
                (
                    run_id,
Confidence
80% confidence
Finding
Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script persists sensitive Spotify data such as user profile details, email, playlists, and listening history into a local SQLite database without any access controls, minimization, encryption, or user-consent mechanism. In an agent skill context, this creates a real privacy and data-retention risk because the data can remain on disk longer than expected and may be accessible to other local users, processes, backups, or logs.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The queue path invokes another script to add tracks to a playback device and then updates the local database to mark items as queued. In this file there is no confirmation prompt, comment/docstring warning, or user-facing disclosure before these state-changing operations occur.

Static analysis

Detected: suspicious.install_untrusted_source

Install source points to URL shortener or raw IP.

Warn
Code
suspicious.install_untrusted_source
Location
config.json:7