T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/suno-client.mjs:12
- Finding
- Unrestricted Service Endpoint Override Can Expose Credentials and Uploaded Files## Vulnerability Details **File Location**: `scripts/suno-client.mjs:12-13, 145, 182, 310, 353, 419, 447-452` **Vulnerability Type**: Unvalidated security-sensitive endpoint override **Risk Level**: High ### Vulnerable Code ```js const DEFAULT_BASE_URL = "https://mcp.suno.cn"; const BASE_URL = (process.env.SUNO_CN_MCP_URL || DEFAULT_BASE_URL).replace(/\/+$/, ""); ``` ```js async function fetchResponse(path, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), options.timeout || 30_000); try { const response = await fetch(`${BASE_URL}${path}`, { ...options, signal: controller.signal }); ``` ```js async function querySkillStatus(credentials) { return fetchJSON("/mcp/auth/status", { method: "GET", headers: { Authorization: `Bearer ${credentials.skill_key}` }, }); } ``` ```js body: JSON.stringify({ skill_key: credentials.skill_key, client_instance_id: credentials.client_instance_id, client_name: CLIENT_NAME, skill_version: SKILL_VERSION, }), ``` ```js const credential = await resolveBusinessCredential(); const headers = { Authorization: `Bearer ${credential.value}` }; ``` ```js await fetchJSON("/mcp/auth/binding", { method: "DELETE", headers: { Authorization: `Bearer ${skillKey}` }, }); ``` ```js const absolutePath = resolve(filePath); let data; try { data = await readFile(absolutePath); } catch { throw new ClientError("file_not_found", "Unable to read the upload file"); } const form = new FormData(); form.append("file", new Blob([data]), basename(absolutePath)); ``` ### Technical Analysis The helper accepts `SUNO_CN_MCP_URL` as the base URL for all remote requests without validating its scheme, hostname, port, or origin. The same generic request function is then used for authorization, account operations, business requests, logout, and file uploads. Consequently, setting this environment variable to an attacker-controlled URL redirects the following security- ...[truncated 2591 chars]
- Remediation
- ## Remediation Suggestions 1. **Pin the production origin** - In normal operation, require the exact origin `https://mcp.suno.cn`. - Reject URLs containing alternate schemes, credentials, unexpected ports, fragments, or unapproved hosts. 2. **Parse and validate the URL** - Use `new URL()` rather than string concatenation. - Require `https:`. - Compare `url.origin` against an explicit allowlist. 3. **Separate development and production modes** - Permit endpoint overrides only when an explicit development-mode flag is enabled. - Do not allow production Skill Keys or legacy API keys to be sent in development mode. - Use separate test credentials for approved staging origins. 4. **Protect sensitive operations** - Refuse authentication, account access, logout, and file uploads when the effective origin is not the official production origin unless the user explicitly approves a recognized staging environment. - Display the effective non-default origin before any credential or file transmission. 5. **Prevent plaintext transmission** - Reject all `http://` endpoints, including localhost exceptions unless a narrowly scoped development mode explicitly requires them and no production credentials are present. 6. **Construct request URLs safely** - Resolve API paths against the validated base URL and verify that the resulting URL retains the approved origin before calling `fetch`. A suitable production policy would resemble: ```js const PRODUCTION_ORIGIN = "https://mcp.suno.cn"; const candidate = new URL(process.env.SUNO_CN_MCP_URL || PRODUCTION_ORIGIN); if (candidate.protocol !== "https:" || candidate.origin !== PRODUCTION_ORIGIN) { throw new Error("Unapproved Suno.cn service origin"); } const BASE_URL = candidate.origin; ``` If staging support is required, use a hardcoded allowlist and distinct non-production credentials rather than accepting an arbitrary environment-provided destination.
