T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/parallel-probe.js:87
- Finding
- SSRF Protection Can Be Bypassed Through DNS Rebinding and Incomplete IPv6 Filtering<![CDATA[ ## Vulnerability Details **File Location**: `scripts/parallel-probe.js`, lines 87–114 **Vulnerability Type**: Server-Side Request Forgery through DNS time-of-check/time-of-use mismatch **Risk Level**: High ### Vulnerable Code ```javascript let resolvedIp; try { const { address } = await dns.lookup(parsed.hostname); resolvedIp = address; } catch { return { url: rawUrl, available: false, error: 'dns_resolution_failed' }; } if (isBlockedHost(resolvedIp)) { return { url: rawUrl, available: false, error: 'blocked_resolved_ip' }; } const lib = parsed.protocol === 'https:' ? https : http; const domain = parsed.hostname.replace(/^www\./, ''); const tier = DOMAIN_TIER[domain] || 'unknown'; const score = REP.scores[tier] || REP.scores.unknown; return new Promise(resolve => { const options = { method: 'HEAD', hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, timeout: TIMEOUT_MS, headers: { 'User-Agent': 'search-skill-probe/1.0 (availability-check)' } }; const req = lib.request(options, res => { ``` The associated address blocklist is incomplete: ```javascript const BLOCKED_HOST_PATTERNS = [ /^localhost$/i, /^127\./, /^0\.0\.0\.0$/, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^169\.254\./, /^::1$/, /^fc[0-9a-f]{2}:/i, /^fe80:/i, /^0::/, ]; ``` ### Technical Analysis The code resolves the supplied hostname with `dns.lookup()` and checks the resulting address against a private-address blocklist. However, the validated address is not used to establish the connection. Instead, `http.request()` or `https.request()` receives the original hostname and may perform another DNS resolution. This creates a time-of-check/time-of-use gap. An attacker who controls DNS for a supplied hostname can return a public address during the explicit validation lookup and an internal address when the HTTP ...[truncated 2061 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve all addresses for the hostname and reject the request if any resolved address is non-public. 2. Use `net.isIP()` and a CIDR-aware IP classification library or equivalent robust implementation rather than regular-expression matching. 3. Block all loopback, private, link-local, unspecified, multicast, reserved, documentation, carrier-grade NAT, and IPv4-mapped IPv6 address ranges as appropriate for the deployment. 4. Pin the validated address to the request by supplying a custom `lookup` function that returns only the previously validated address. 5. For HTTPS, preserve the original hostname for TLS SNI and certificate validation while connecting only to the pinned address. 6. Revalidate the connected socket's remote address after connection establishment and terminate the connection if it differs from the approved address. 7. Restrict destination ports to an explicit allowlist, normally ports 80 and 443, unless other ports are required. 8. Add regression tests for DNS rebinding, IPv4-mapped IPv6, `fd00::/8`, loopback, link-local, and cloud metadata destinations. 9. Correct the security manifest so it does not claim complete DNS-rebinding prevention until address pinning is implemented. ]]>
