T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/horizon.py:56
- Finding
- Configurable Feed URLs Permit SSRF Through DNS and IPv6 Validation Bypasses<![CDATA[ ## Vulnerability Details **File Location**: `scripts/horizon.py`, lines 56-90 and 386-410 **Vulnerability Type**: Server-Side Request Forgery caused by incomplete destination validation **Risk Level**: High ### Vulnerable Code ```python # Blocked hostnames / IP patterns for SSRF prevention. _BLOCKED_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "[::1]", "metadata.google.internal"} def _is_private_ip(hostname: str) -> bool: """Check if hostname looks like a private/internal IP address.""" parts = hostname.split(".") if len(parts) != 4: return False try: octets = [int(p) for p in parts] except ValueError: return False if octets[0] == 10: return True if octets[0] == 172 and 16 <= octets[1] <= 31: return True if octets[0] == 192 and octets[1] == 168: return True if octets[0] == 169 and octets[1] == 254: return True return False def _validate_public_url(url_str: str, label: str) -> str: """Validate that a URL is HTTPS and targets a public host (not internal/private).""" from urllib.parse import urlparse parsed = urlparse(url_str) if parsed.scheme not in ("https",): _print({"error": f"{label} must use HTTPS"}) sys.exit(1) hostname = (parsed.hostname or "").lower() if not hostname: _print({"error": f"{label} has no hostname"}) sys.exit(1) if hostname in _BLOCKED_HOSTS or _is_private_ip(hostname): _print({"error": f"{label} cannot target private/internal addresses"}) sys.exit(1) return url_str ``` The validation is applied to user-configurable feeds as follows: ```python elif cmd == "start-feed": if len(args) < 3: _print({"error": "usage: start-feed <name> <feed_type> [config_json]"}) sys.exit(1) name = _validate_id(args[1], "feed_name") feed_type = _validate_id(args[2], "feed_type") if feed_type not in _VALID_FEED_TYPES: _print({"error": f ...[truncated 3820 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse the URL and reject embedded credentials, malformed ports, fragments, and ambiguous host syntax. 2. Resolve the hostname with `socket.getaddrinfo()` before making a connection. 3. Convert every resolved address through Python's `ipaddress.ip_address()` and require `is_global` to be true. 4. Reject loopback, private, link-local, multicast, reserved, unspecified, and IPv4-mapped non-global addresses for both IPv4 and IPv6. 5. Validate every redirect destination with the same policy, or disable redirects. 6. Prevent DNS rebinding by connecting to a validated resolved address while preserving the intended TLS hostname for certificate and SNI verification. 7. Prefer an allowlist of approved RPC and REST API domains where operationally possible. 8. Apply outbound firewall or proxy controls so the process cannot reach instance metadata, private networks, or loopback services regardless of application validation. 9. Add tests covering DNS-to-private resolution, IPv6 loopback, unique-local and link-local IPv6, IPv4-mapped IPv6, redirects, and rebinding scenarios. ]]>
