T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/render-gotchi-bypass.mjs:197
- Finding
- Renderer Response Permits Server-Side Request Forgery Through Unrestricted Asset URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/render-gotchi-bypass.mjs`, lines 197–204 and 248–263 **Vulnerability Type**: Unrestricted server-side URL fetching **Risk Level**: Medium ### Vulnerable Code ```js async function downloadFile(url, filePath) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download ${url} (${response.status})`); } const bytes = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(filePath, bytes); return filePath; } ``` ```js if (proxyUrls.PNG_Full) { const fullUrl = proxyUrls.PNG_Full.startsWith("http") ? proxyUrls.PNG_Full : `${DAPP_BASE}${proxyUrls.PNG_Full}`; artifacts.fullPngPath = path.join(options.outDir, `gotchi-${tokenId}-full.png`); await downloadFile(fullUrl, artifacts.fullPngPath); artifacts.fullPngUrl = fullUrl; } if (proxyUrls.PNG_Headshot) { const headshotUrl = proxyUrls.PNG_Headshot.startsWith("http") ? proxyUrls.PNG_Headshot : `${DAPP_BASE}${proxyUrls.PNG_Headshot}`; artifacts.headshotPngPath = path.join(options.outDir, `gotchi-${tokenId}-headshot.png`); await downloadFile(headshotUrl, artifacts.headshotPngPath); artifacts.headshotPngUrl = headshotUrl; } ``` ### Technical Analysis The renderer API controls the `proxyUrls.PNG_Full` and `proxyUrls.PNG_Headshot` values. Any value beginning with `http` is passed directly to `fetch` without validating its protocol, hostname, port, resolved IP address, or destination network. Node.js `fetch` follows redirects by default. Consequently, even an initially acceptable public URL can redirect the request to a loopback, private-network, link-local, or cloud metadata address. The implementation also performs no response content-type validation before saving the returned bytes. The external renderer request is necessary for the Skill's declared rendering functionality. However, allowing the renderer response to select arbitrary network destinations exceeds the minimum n ...[truncated 1772 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse every asset URL using `new URL()` rather than checking whether the string starts with `http`. 2. Require the `https:` protocol. 3. Maintain an explicit allowlist of trusted Aavegotchi asset hostnames. 4. Reject embedded usernames, passwords, nonstandard ports, and malformed URLs. 5. Resolve destination hostnames and reject loopback, private, link-local, multicast, and otherwise reserved IPv4 and IPv6 ranges. 6. Set `redirect: "manual"` and validate every redirect destination before following it. 7. Apply request timeouts and maximum download-size limits. 8. Validate that the response has an expected image content type before writing it. 9. Prefer having the trusted renderer return relative asset paths that are resolved only against a fixed, trusted base URL. Example hardening pattern: ```js const ALLOWED_ASSET_HOSTS = new Set([ "www.aavegotchi.com" ]); function validateAssetUrl(value) { const url = new URL(value, DAPP_BASE); if (url.protocol !== "https:") { throw new Error("Asset URL must use HTTPS."); } if (!ALLOWED_ASSET_HOSTS.has(url.hostname)) { throw new Error(`Untrusted asset host: ${url.hostname}`); } if (url.username || url.password || url.port) { throw new Error("Asset URL contains prohibited authority components."); } return url; } ``` DNS resolution and redirect validation must also be implemented to prevent DNS rebinding and redirect-based bypasses. ]]>
