T09 · Insecure Skill Coding Practices
- Location
scripts/launch.js:37- Finding
Server-Side Request Forgery Through Unrestricted Product Image Downloads
- Content
View full analysis
{ const proto = url.startsWith('https') ? https : http; const file = fs.createWriteStream(dest); proto.get(url, res => { if (res.statusCode === 301 || res.statusCode === 302) { file.close(); fs.unlink(dest, () => {}); return downloadImage(res.headers.location, dest).then(resolve).catch(reject); } if (res.statusCode !== 200) { file.close(); fs.unlink(dest, () => {}); return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); } res.pipe(file); file.on('finish', () => file.close(resolve)); file.on('error', reject); }).on('error', reject); }); } async function downloadImages(imageUrls, productId) { fs.mkdirSync(IMAGE_DIR, { recursive: true }); const dir = path.join(IMAGE_DIR, productId.replace(/[^a-z0-9]/gi, '_')); fs.mkdirSync(dir, { recursive: true }); const localPaths = []; for (let i = 0; i < imageUrls.length; i++) { const url = imageUrls[i]; const ext = (url.split('?')[0].match(/\.(jpg|jpeg|png|webp|gif)$/i) || ['', '.jpg'])[1]; const dest = path.join(dir, `image_${i + 1}${ext}`); try { console.log(` ⬇ Downloading image ${i + 1}/${imageUrls.length}...`); await downloadImage(url, dest); localPaths.push(dest); } catch (e) { console.warn(` ⚠ Failed to download image ${i + 1}: ${e.message}`); } } return localPaths; } ``` ### Technical Analysis Product image URLs come from the CJ API response and are passed directly to Node.js HTTP clients. The code does not validate the URL scheme, destination hostname, resolved IP address, port, or response content type. Both HTTP and HTTPS requests ar ...[truncated 1835 chars]- Remediation
View remediation
