T09 · Insecure Skill Coding Practices
Error
- Location
- skill.js:9
- Finding
- Paragraph Credentials and Sensitive Request Data Can Be Redirected to an Arbitrary Host<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:9-75` **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 } // Set up abort controller for timeout const controller = new AbortController() const timeoutMs = options.timeout || 30000 // default 30 seconds (POSTs can be slow) 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: "PO ...[truncated 2802 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Remove the production base-URL override unless it is operationally essential. 2. For production use, hard-code and validate the destination: ```javascript const OFFICIAL_API_ORIGIN = "https://public.api.paragraph.com" const API_BASE = `${OFFICIAL_API_ORIGIN}/api` ``` 3. If test endpoints must remain supported: - Require an explicit development/test-mode flag. - Maintain an allowlist of approved HTTPS origins. - Reject URLs containing credentials, non-HTTPS protocols, unexpected ports, or unapproved hosts. - Use separate test credentials rather than the production API key. 4. Before attaching `Authorization`, compare `url.origin` to the expected credential origin: ```javascript if (url.origin !== "https://public.api.paragraph.com") { throw new Error("Refusing to send Paragraph credentials to an unapproved origin") } ``` 5. Apply the same centralized request validation to CSV imports rather than constructing a second direct `fetch` request. 6. Document the exact network destination and credential scope, and encourage users to create narrowly scoped, revocable API keys. 7. Add tests proving that HTTP URLs, alternate domains, user-info URLs, and unapproved ports are rejected before any network request occurs. ]]>
