T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/csv-import/import-shopify-csv.js:282
- Finding
- Server-Side Request Forgery Through CSV-Controlled Image URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/csv-import/import-shopify-csv.js:282-320` **Vulnerability Type**: Server-Side Request Forgery and potential internal-data relay **Risk Level**: High ### Vulnerable Code ```js function getExt(url) { const m = url.match(/\.(\w{3,4})(\?|$)/); return m ? m[1] : 'jpg'; } function downloadImage(url) { return new Promise((resolve, reject) => { const transport = url.startsWith('https:') ? https : http; transport.get(url, { timeout: 30000 }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { return downloadImage(res.headers.location).then(resolve).catch(reject); } if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`)); const bufs = []; res.on('data', d => bufs.push(d)); res.on('end', () => resolve(Buffer.concat(bufs))); }).on('error', reject).on('timeout', () => reject(new Error('Timeout'))); }); } async function uploadImages(images, productTitle, imgConcurrency = 5, imgRetries = 3) { const slugify = s => s.replace(/[^a-zA-Z0-9\u4e00-\u9fff-]/g, '_').substring(0, 30); const results = await concurrentMap(images, imgConcurrency, async (img) => { let lastErr; for (let attempt = 0; attempt <= imgRetries; attempt++) { try { const buf = await downloadImage(img.src); const ext = getExt(img.src); const r = await api.post('/api/skill/base-image/upload', { image_base64encode: buf.toString('base64'), image_name: `${slugify(productTitle)}_${img.position}.${ext}`, group_type: 'product' }); ``` ### Technical Analysis The importer obtains `img.src` from the CSV `Image Src` column and passes it directly to `http.get` or `https.get`. It performs no validation of: - The URL scheme - The destination hostname - Resolved IP addresses - Loopback, link-local, private, or reserved address ranges - Redirect destinations - ...[truncated 1915 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse every image URL with `new URL()` and reject parsing failures. 2. Permit only `https:` unless a narrowly scoped development option explicitly allows HTTP. 3. Resolve the hostname before connecting and reject every address in loopback, private, link-local, multicast, unspecified, documentation, and reserved ranges for both IPv4 and IPv6. 4. Repeat the complete scheme, hostname, DNS, and IP validation for every redirect. 5. Set a small redirect limit rather than using unrestricted recursion. 6. Consider an allowlist of approved image CDN domains. 7. Reject responses whose declared or detected MIME type is not an approved image format. 8. Apply strict response-size and timeout limits. 9. Where possible, perform image decoding and re-encoding before upload so arbitrary response bytes cannot be relayed unchanged. 10. Protect against DNS rebinding by connecting to a previously validated resolved address while preserving the expected TLS server name. ]]>
