Back to skill

Security audit

Playlistable

Security checks for vulnerabilities and agentic risk

Overview

The skill appears purpose-built for Spotify playlist management, but it warrants review because it stores a Spotify-connected bearer key locally and exposes playlist deletion without clear confirmation guidance.

Review before installing if you care about protecting Spotify playlist access. Treat config/auth.json as a secret, avoid committing or sharing it, and require a clear user confirmation before running delete_playlist. Also note that generated playlists are documented as public on Spotify.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth.mjs:91
Finding
Bearer API Key Stored Without Explicit Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth.mjs`, lines 91-100 **Vulnerability Type**: Insecure credential storage **Risk Level**: Medium ### Vulnerable Code ```js const { access_token } = await tokenResp.json(); // Save to config if (!existsSync(CONFIG_DIR)) { mkdirSync(CONFIG_DIR, { recursive: true }); } writeFileSync( CONFIG_PATH, JSON.stringify({ api_key: access_token, created_at: new Date().toISOString() }, null, 2) ); ``` ### Technical Analysis The OAuth access token is a bearer credential that authorizes Playlistable MCP operations. The script saves it to `config/auth.json` without specifying a restrictive filesystem mode. The resulting permissions depend on the process umask and operating-system defaults. In a multi-user or otherwise insufficiently isolated environment, the file may be readable by local principals that do not require access to the Playlistable account. The credential is not transmitted to an unrelated host: its exchange through the fixed HTTPS endpoint `https://mcp.playlistable.io/oauth/token` is necessary for the documented OAuth workflow. The vulnerability concerns how the resulting credential is persisted locally. ### Attack Path 1. A victim runs `node scripts/auth.mjs` and completes OAuth authentication. 2. The returned bearer API key is written to `config/auth.json` using ambient filesystem permissions. 3. Another local user or compromised process with access to the project directory reads the file. 4. The attacker extracts the `api_key` value. 5. The attacker supplies the key as an `Authorization: Bearer` credential to `https://mcp.playlistable.io`. 6. The attacker invokes the MCP operations available to the victim's account. This path requires local filesystem access sufficient to read the file; remote exploitation is not established by the audited code alone. ### Impact Assessment A stolen bearer key can allow impersonation of the authenticated Playlistable user within the token's server ...[truncated 440 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the credential directory with owner-only permissions: ```js mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); ``` 2. Write the credential file with mode `0600`: ```js writeFileSync( CONFIG_PATH, JSON.stringify( { api_key: access_token, created_at: new Date().toISOString() }, null, 2 ), { mode: 0o600 } ); ``` 3. Explicitly enforce permissions on an existing directory and file, since creation modes do not correct pre-existing permissive paths. 4. Defend against symlink-based redirection by validating the destination and using safe, exclusive or atomic file-creation practices. 5. Prefer an operating-system credential store or secret manager where available. 6. Remove token-fragment logging so credential material is never unnecessarily exposed in terminal output. 7. Document credential revocation and rotation procedures for users who suspect local disclosure. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/auth.mjs:61
Finding
Unescaped OAuth and Upstream Error Data Reflected into Local HTML Responses<![CDATA[ ## Vulnerability Details **File Locations**: `scripts/auth.mjs`, lines 61-68 and 107-112 **Vulnerability Type**: Reflected HTML/script injection **Risk Level**: Low ### Vulnerable Code Callback error reflection at lines 61-68: ```js const code = url.searchParams.get("code"); if (!code) { const error = url.searchParams.get("error") || "unknown"; res.writeHead(400, { "Content-Type": "text/html" }); res.end(`<h1>Auth failed</h1><p>${error}</p>`); console.error(`\nAuth failed: ${error}`); ``` Token-exchange error reflection at lines 107-112: ```js } catch (err) { res.writeHead(500, { "Content-Type": "text/html" }); res.end(`<h1>Error</h1><p>${err.message}</p>`); console.error(`\nToken exchange error: ${err.message}`); } ``` ### Technical Analysis The callback handler places the `error` query parameter directly into an HTML response without contextual escaping. The token-exchange failure path similarly inserts `err.message`, which may include response content returned by the remote token endpoint. Because the response is declared as `text/html`, attacker-controlled markup can be interpreted by the browser. A payload containing HTML or script could therefore execute in the temporary localhost origin. The exposure is reduced because the server: - Binds only to `127.0.0.1`. - Uses a randomly selected port. - Exists only during the authentication flow. - Closes after handling the callback. These constraints make the issue lower risk, but they do not make unescaped insertion into HTML safe. ### Attack Path One applicable callback path is: 1. The authentication helper starts its temporary loopback HTTP server. 2. An attacker or compromised authorization component causes the browser to request the callback URL with an attacker-controlled `error` query value. 3. The callback handler interpolates that value into the HTML response without escaping it. 4. The browser parses the value as active HTML rather than plain text. 5. Attacker-cont ...[truncated 1008 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer plain-text error responses so browser HTML interpretation is unnecessary: ```js res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8", "X-Content-Type-Options": "nosniff", }); res.end(`Authentication failed: ${error}`); ``` 2. If HTML responses are required, apply correct HTML text-context escaping to every dynamic value before interpolation. 3. Do not display raw upstream response bodies to the browser. Return a generic message and record sanitized diagnostic details separately. 4. Add a restrictive Content Security Policy, for example: ```http Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline' ``` 5. Set `X-Content-Type-Options: nosniff` on all callback responses. 6. Keep the loopback-only binding, random port, short timeout, and immediate server shutdown, as these appropriately reduce exposure. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (5)

Ae1

High
Category
analysis-evasion
Content
`https://mcp.playlistable.io` exposes tools via Streamable HTTP transport. The `mcp-call.mjs` script sends JSON-RPC requests directly — no MCP SDK needed.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill documents use of environment variables and outbound network access, but it does not declare any explicit tool scope or permission boundaries. That creates an authorization transparency gap: an agent or reviewer cannot easily verify what external access the skill is expected to use, increasing the risk of over-privileged execution or unnoticed data egress.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The authentication instructions state that an API key is automatically saved to `config/auth.json`, but they do not warn about local secret storage, file permissions, or the sensitivity of the credential. This can lead to inadvertent exposure through weak filesystem permissions, accidental commits, or multi-user environments.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill exposes a `delete_playlist` action as a normal example without warning that it is destructive or instructing the agent to require explicit user confirmation first. In an agent setting, this increases the chance of accidental or prompt-induced deletion of user data without adequate consent.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The API reference documents a destructive `delete_playlist` operation that removes playlists from both Playlistable and Spotify, but it does not include any warning about irreversible impact or a recommendation for explicit user confirmation before use. In an agent context, this increases the risk of accidental or unauthorized destructive actions being taken on behalf of a user through ambiguous prompts, automation mistakes, or prompt injection influencing tool selection.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/auth.mjs:162