T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/scanner.py:174
- Finding
- SSRF Validation Fails Open When DNS Resolution Fails or Times Out<![CDATA[ ## Vulnerability Details **File Location**: `scripts/scanner.py:174-213` **Vulnerability Type**: Server-Side Request Forgery validation bypass **Risk Level**: High ### Vulnerable Code ```python def _validate_url_safe(url): """验证 URL 是否指向内网/私有网络。 如果不安全则抛出 SSRFError。""" parsed = urlparse(url) # 仅允许 http 和 https 协议 if parsed.scheme not in ('http', 'https'): raise SSRFError(f"Blocked scheme: {parsed.scheme} (only http/https allowed)") hostname = parsed.hostname if not hostname: raise SSRFError(f"No hostname in URL: {url}") # 将主机名解析为 IP 并检查是否在屏蔽范围内 # 使用线程池限制 DNS 解析时间,防止卡死 try: with ThreadPoolExecutor(max_workers=1) as dns_executor: future = dns_executor.submit( socket.getaddrinfo, hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM ) addr_infos = future.result(timeout=DNS_RESOLVE_TIMEOUT) except (socket.gaierror, TimeoutError): # DNS 无法解析或超时 — 放行,让 HTTP 请求自然失败 return for family, _, _, _, sockaddr in addr_infos: ip_str = sockaddr[0] try: ip = ipaddress.ip_address(ip_str) except ValueError: continue for blocked in _BLOCKED_IP_RANGES: if ip in blocked: raise SSRFError( f"Blocked: {hostname} resolves to {ip} (private/internal network)" ) ``` ### Technical Analysis The SSRF validator explicitly returns successfully when DNS resolution fails or exceeds its timeout. This is a fail-open security decision: the absence of a successful security check is treated as authorization to continue. After the validator returns, the HTTP client performs its own DNS lookup while establishing the connection. Consequently, a hostname that could not be verified by the preliminary lookup may still be resolved and contacted by `requests`. The HTTP request can therefore proceed without any verified assurance that its ...[truncated 1679 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Fail closed for every DNS failure or timeout: ```python except (socket.gaierror, TimeoutError) as exc: raise SSRFError(f"Unable to safely resolve {hostname}") from exc ``` 2. Resolve the hostname once and retain the validated addresses for the actual connection. Do not permit the HTTP library to independently resolve the hostname again. 3. Reject a hostname if any returned address is private, loopback, link-local, multicast, unspecified, reserved, or otherwise non-global. Prefer `ip.is_global` over a manually maintained partial denylist. 4. Connect only to a validated IP while preserving the original hostname for the HTTP `Host` header and HTTPS SNI/certificate verification. 5. Validate every redirect destination using the same fail-closed and address-pinning process. 6. Verify the actual connected peer address before accepting response data. 7. Disable environment-derived proxy settings unless explicitly required and secured, because a proxy can invalidate assumptions made by local DNS checks. 8. Add tests covering DNS timeouts, DNS errors, mixed public/private answers, redirect destinations, IPv4 and IPv6 addresses, and cloud metadata endpoints. ]]>
