T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/generate.js:196
- Finding
- Unvalidated Server-Controlled URLs Enable Client-Side SSRF and Unbounded Downloads<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.js`, lines 196–207 and 236–250 **Vulnerability Type**: Server-Side Request Forgery from the client environment, unrestricted redirects, and unbounded resource consumption **Risk Level**: Medium ### Vulnerable Code ```javascript const fullStatusUrl = data.statusUrl?.startsWith('http') ? data.statusUrl : `https://sideload.gg${data.statusUrl}`; let result = null; for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) { await new Promise(r => setTimeout(r, POLL_INTERVAL)); const statusRes = await fetch(fullStatusUrl); const statusData = await statusRes.json(); ``` The same trust issue affects result downloads: ```javascript const downloads = [ { url: result.glbUrl, ext: '.glb', label: 'GLB' }, { url: result.vrmUrl, ext: '.vrm', label: 'VRM' }, { url: result.processedImageUrl, ext: '.png', label: 'PNG' }, ]; console.log('📥 Downloading...'); for (const { url, ext, label } of downloads) { if (!url) continue; try { const res = await fetch(url); if (res.ok) { const buffer = Buffer.from(await res.arrayBuffer()); const filePath = join(OUTPUT_DIR, `${baseName}${ext}`); writeFileSync(filePath, buffer); console.log(` ✅ ${label}: ${filePath}`); } } catch (e) { console.log(` ⚠️ ${label}: ${e.message}`); } } ``` ### Technical Analysis The generation API controls `statusUrl`, `glbUrl`, `vrmUrl`, and `processedImageUrl`. The script performs requests to these values without validating: - URL scheme - Destination hostname - Destination port - Resolved IP address - Redirect destinations - Response content type - Response or download size An absolute `statusUrl` is accepted whenever it begins with `http`, including plaintext HTTP and arbitrary external or internal hosts. Result URLs are accepted with no validation at all. Node.js `fetch` also follows redirects by default, so validating only an initial URL would not be sufficie ...[truncated 2277 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Require HTTPS for every status and asset URL. 2. Maintain separate allowlists for the status endpoint and documented asset hosts. 3. Parse URLs with `new URL()` and reject embedded credentials, fragments, unexpected ports, and malformed values. 4. Resolve destination hostnames and reject loopback, private, link-local, multicast, unspecified, and reserved IPv4 and IPv6 ranges. 5. Disable automatic redirects or validate every redirect destination against the same rules. 6. Prefer deriving the status URL locally from the validated `jobId` rather than trusting `data.statusUrl`. 7. Add request timeouts with `AbortController`. 8. Enforce maximum `Content-Length` and streamed-byte limits before writing downloads. 9. Stream assets directly to files instead of loading the entire response into memory. 10. Validate expected content types and optionally verify file signatures for PNG, GLB, and VRM files. 11. Apply a total download quota and remove partial files when a request fails. Example status URL construction: ```javascript const jobIdPattern = /^avt-[A-Za-z0-9-]+$/; if (!jobIdPattern.test(jobId)) { throw new Error('Invalid job ID'); } const fullStatusUrl = `https://sideload.gg/api/agent/generate/${encodeURIComponent(jobId)}/status`; ``` ]]>
