T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/mirror.py:44
- Finding
- Private-network SSRF through unchecked DNS resolution and redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mirror.py:44-73`, with the validated URL passed to HTTrack at `scripts/mirror.py:112-118` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: Medium ### Vulnerable Code ```python def check_url(u, allow_private=False): u = u.strip() if any(c.isspace() for c in u): raise UsageError("url contains whitespace") p = urllib.parse.urlparse(u) if p.scheme not in ("http", "https"): raise UsageError(f"scheme {p.scheme or '(none)'} refused — only http/https are mirrored") if not p.netloc: raise UsageError("url has no host") if "@" in p.netloc: raise UsageError("userinfo (user@host) in url refused — phishing-shaped hosts are refused") if not allow_private: host = p.hostname or "" if host.lower() == "localhost" or _is_private_ip(host): raise UsageError( f"host {host!r} is loopback/link-local/private — refused by default; " "pass --allow-private for authorized LAN mirrors") return u.split("#", 1)[0] def _is_private_ip(host): import ipaddress h = host.strip("[]") try: ip = ipaddress.ip_address(h) except ValueError: return False # a DNS name — can't classify lexically return (ip.is_loopback or ip.is_link_local or ip.is_private or ip.is_multicast or ip.is_unspecified) ``` The accepted URL is subsequently passed to HTTrack: ```python def run_httrack(argv_extra, timeout): b = find_binary() if not b: raise SystemExit(3) argv = [b] + argv_extra t0 = time.time() try: p = subprocess.run(argv, capture_output=True, timeout=timeout or None) ``` ### Technical Analysis The private-network restriction examines only literal IP addresses. When the URL contains a DNS hostname, `ipaddress.ip_address()` raises `ValueError`, and `_is_private_ip()` immediately returns `False`. Th ...[truncated 2421 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve the URL hostname before invoking HTTrack using `socket.getaddrinfo()`. 2. Validate every returned IPv4 and IPv6 address. Reject loopback, private, link-local, multicast, unspecified, reserved, and otherwise non-global addresses unless `--allow-private` was explicitly supplied. 3. Treat mixed DNS results as unsafe: reject the hostname if any returned address is non-global. 4. Do not rely only on pre-resolution, because DNS can change between validation and connection. Pin the validated address where possible while preserving the intended HTTP `Host` header and TLS server name. 5. Validate every redirect target before following it. If HTTrack cannot provide a reliable redirect-validation hook, run it behind an egress proxy or network policy that blocks private, loopback, link-local, metadata, and reserved address ranges. 6. Apply the same controls to both initial URLs and asset/link requests discovered during crawling. 7. Add regression tests for: - Hostnames resolving to `127.0.0.1` - RFC1918 IPv4 destinations - IPv6 loopback, unique-local, and link-local destinations - Hostnames returning mixed public and private addresses - Public endpoints redirecting to private destinations - DNS responses changing between validation and connection 8. Update the README and manifest to avoid claiming that network access is limited to caller-supplied public URLs until destination enforcement covers DNS resolution and redirects. ]]>
