T09 · Insecure Skill Coding Practices
- Location
- skill.js:9
- Finding
- Configurable API Base URL Can Exfiltrate API Credentials and Sensitive Data<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:9`, `skill.js:35-76`, and `skill.js:447-459` **Vulnerability Type**: Unrestricted credential-bearing network 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 } 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 CSV import path independently uses the same configurable destination: ```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", headers: { "Authorization": `Bea ...[truncated 2857 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Remove `PARAGRAPH_API_BASE_URL` from production configuration and use the fixed official origin: ```javascript const API_BASE = "https://public.api.paragraph.com/api" ``` 2. If endpoint overriding is essential for development, enforce an explicit allowlist: ```javascript const allowedOrigins = new Set([ "https://public.api.paragraph.com" ]) const baseUrl = new URL( process.env.PARAGRAPH_API_BASE_URL || "https://public.api.paragraph.com/api" ) if (!allowedOrigins.has(baseUrl.origin)) { throw new Error("Unapproved Paragraph API origin") } ``` 3. Reject non-HTTPS destinations, embedded URL credentials, unexpected ports, and malformed URLs. 4. Prevent cross-origin redirect credential leakage. Use a restrictive redirect policy such as `redirect: "error"`, or manually verify the destination origin before following a redirect. 5. Separate production and test modes. Test mode should require: - An explicit opt-in flag. - Dedicated non-production API credentials. - A narrowly defined test-host allowlist. - A safeguard preventing production-format credentials from being used with test hosts. 6. Validate the final URL immediately before each request rather than only validating configuration during module initialization. 7. Clearly document which data each tool sends externally and require user confirmation for especially sensitive operations such as subscriber imports. ]]>
