T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/manus_slides_json_to_pptx.mjs:20
- Finding
- Unrestricted Slide Asset Downloader Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manus_slides_json_to_pptx.mjs`, lines 20-40 and 55-65 **Vulnerability Type**: Server-Side Request Forgery and unsafe file download **Risk Level**: High ### Vulnerable Code ```javascript function download(url, dest) { const client = url.startsWith('https:') ? https : http; return new Promise((resolve, reject) => { const req = client.get(url, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); return resolve(download(res.headers.location, dest)); } if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); } const file = fs.createWriteStream(dest); res.pipe(file); file.on('finish', () => file.close(() => resolve(dest))); file.on('error', reject); }); req.on('error', reject); req.setTimeout(120000, () => req.destroy(new Error('timeout'))); }); } ``` The untrusted URL is obtained from the slide JSON and passed directly to the downloader: ```javascript const slides = Array.isArray(obj.slide_ids) ? obj.slide_ids : []; for (let i = 0; i < slides.length; i++) { const slideId = slides[i]; const slide = pptx.addSlide(); const imgUrl = obj.images?.[slideId]; const outline = Array.isArray(obj.outline) ? obj.outline.find((x) => x.id === slideId) : undefined; if (imgUrl) { const imgPath = path.join(outDir, `${String(i + 1).padStart(2, '0')}_${slideId}.png`); if (!fs.existsSync(imgPath)) { await download(imgUrl, imgPath); } ``` ### Technical Analysis The downloader accepts asset URLs directly from an input JSON document. It does not enforce HTTPS, restrict destination hosts to Manus-controlled domains, reject private or link-local IP addresses, restrict destination ports, or limit redirect depth. Any URL not beginning with `https:` is handled by the HTTP client. Redirect destinations ...[truncated 1759 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Require `https:` for every initial and redirected URL. - Restrict hosts to an exact, centrally maintained allowlist of Manus-controlled domains. - Reject URLs containing user information, unexpected ports, malformed hostnames, or ambiguous IP representations. - Resolve hostnames and reject loopback, private, link-local, multicast, and reserved IPv4 and IPv6 addresses. - Revalidate every redirect destination before following it. - Set a small maximum redirect count. - Stream downloads with a strict byte limit instead of accepting unlimited responses. - Verify the response content type and image format before using the asset. - Write to a temporary file and atomically rename it only after validation succeeds. - Remove partial files after timeout, size-limit, network, or validation failures. - Reuse one common downloader implementation so that the Python collector and JavaScript converter enforce the same security policy. ]]>
