T09 · Insecure Skill Coding Practices
Error
- Location
- skill.js:9
- Finding
- Configurable API Base URL Enables Bearer Credential and Sensitive Data Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:9`, `skill.js:33-76`, and `skill.js:447-459` **Vulnerability Type**: Unrestricted sensitive-data transmission destination **Risk Level**: High ### Vulnerable Code ```javascript const API_BASE = process.env.PARAGRAPH_API_BASE_URL || "https://public.api.paragraph.com/api" ``` ```javascript async function request(method, endpoint, body = null, params = {}, options = {}) { // Read API_KEY from env at call time to respect per-skill injection const apiKey = process.env.PARAGRAPH_API_KEY if (!apiKey) { throw new Error("PARAGRAPH_API_KEY environment variable not set") } const url = new URL(`${API_BASE}${endpoint}`) Object.keys(params).forEach(key => { if (params[key] !== undefined && params[key] !== null) { url.searchParams.append(key, String(params[key])) } }) const headers = { "Authorization": `Bearer ${apiKey}` } let fetchBody = null if (body) { headers["Content-Type"] = "application/json" fetchBody = JSON.stringify(body) } if (options.rawBody) { fetchBody = options.rawBody Object.assign(headers, options.headers) } else if (options.formData) { fetchBody = options.formData // Don't set Content-Type; fetch will set boundary } // Set up abort controller for timeout const controller = new AbortController() const timeoutMs = options.timeout || 30000 const timeoutId = setTimeout(() => controller.abort(), timeoutMs) try { const response = await fetch(url.toString(), { method, headers, body: fetchBody, signal: controller.signal }) ``` The same configurable destination is used for subscriber imports: ```javascript const url = new URL(`${API_BASE}/v1/subscribers/import`) url.searchParams.append('sendWelcomeEmail', sendWelcomeEmail) const formData = new FormData() formData.append('file', csvBuffer, 'subscribers.csv') const response = await fetch(url.toString(), { method: "POST", hea ...[truncated 2481 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Pin production requests to the official endpoint: ```javascript const PARAGRAPH_API_BASE = "https://public.api.paragraph.com/api" ``` 2. If endpoint overrides are required for development, require an explicit non-production mode and reject overrides by default: ```javascript const allowedHosts = new Set(["public.api.paragraph.com"]) const apiUrl = new URL(configuredBase) if (apiUrl.protocol !== "https:" || !allowedHosts.has(apiUrl.hostname)) { throw new Error("Unapproved Paragraph API endpoint") } ``` 3. Never send a production API key to a custom test endpoint. Require a separate test credential variable when development mode is enabled. 4. Normalize the URL and reject embedded credentials, redirects to unapproved hosts, nonstandard schemes, and unexpected ports. 5. Configure `fetch` with manual redirect handling or validate every redirect target before forwarding an `Authorization` header. 6. Display or record the validated destination without logging credentials, and require explicit administrative approval before enabling custom endpoints. 7. Apply equivalent destination validation to the separate CSV-import `fetch` implementation. ]]>
