T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/fetch-markdown.mjs:15
- Finding
- Bypassable SSRF Protection Allows Access to Internal Network Resources<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-markdown.mjs:15-28, 121-140` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```js function isPrivateHost(hostname) { const h = hostname.toLowerCase(); if (["localhost", "127.0.0.1", "::1", "0.0.0.0"].includes(h)) return true; const ipv4 = h.match(/^(\d{1,3}\.){3}\d{1,3}$/); if (!ipv4) return false; const [a, b] = h.split(".").map(Number); if (a === 10) return true; if (a === 127) return true; if (a === 169 && b === 254) return true; if (a === 172 && b >= 16 && b <= 31) return true; if (a === 192 && b === 168) return true; return false; } ``` ```js const res = await fetch(u, { signal: ac.signal, redirect: "follow", headers: { "user-agent": "web-markdown-navigator/1.0 (+OpenClaw skill)", accept: "text/html,application/xhtml+xml", }, }); fetchedUrl = res.url || fetchedUrl; const finalUrl = new URL(fetchedUrl); if (isPrivateHost(finalUrl.hostname)) { printErr("ERROR: redirected to private/local host"); process.exit(2); } ``` ### Technical Analysis The URL safety check only blocks selected literal IPv4 addresses and the exact hostnames `localhost`, `127.0.0.1`, `::1`, and `0.0.0.0`. It does not resolve hostnames and validate their A and AAAA records before establishing a connection. Consequently, a public-looking hostname that resolves to a private, loopback, link-local, reserved, or cloud metadata address passes validation. The check also lacks comprehensive IPv6 filtering. IPv6 loopback aliases, unique-local addresses, link-local addresses, IPv4-mapped IPv6 addresses, and other non-public address forms are not rejected. Redirect processing is unsafe because `redirect: "follow"` permits the HTTP client to contact every redirect destination automatically. The final URL is checked only after the redirect chain has already been followed and the destination has been contacted. Although the respo ...[truncated 1990 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve the destination hostname before making a request and validate every returned A and AAAA address. 2. Reject all loopback, private, link-local, multicast, unspecified, reserved, documentation, carrier-grade NAT, and cloud metadata address ranges for both IPv4 and IPv6. 3. Disable automatic redirects by using `redirect: "manual"`. 4. For each redirect, resolve and validate the new destination before sending the next request. 5. Limit the number of redirects and reject protocol changes or destinations containing unexpected credentials. 6. Protect against DNS rebinding by binding the connection to the exact address that was validated rather than resolving the hostname again during connection establishment. 7. Apply outbound firewall or proxy rules that deny access to internal and metadata networks, providing defense in depth. 8. Consider an explicit allowlist of permitted public domains when the deployment context permits it. 9. Add regression tests covering: - Hostnames resolving to private IPv4 addresses. - IPv6 loopback, unique-local, and link-local addresses. - IPv4-mapped IPv6 addresses. - Public-to-private redirect chains. - Multi-step redirects. - DNS rebinding scenarios. ]]>
