{"record":{"id":"05e41e8b00d2e231","repo":"JuliusBrussee/caveman","slug":"ssrf-host-q-resolved-to-no-addresses","errorCode":null,"errorMessage":"ssrf: host %q resolved to no addresses","messagePattern":"ssrf: host %q resolved to no addresses","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":223,"sourceCode":"\t}\n\n\t// \"localhost\" is explicitly blocked regardless of what DNS says — unless a\n\t// self-hosted operator allowlisted it (resolution still runs, so every\n\t// resolved address is range-checked below like any other).\n\tif strings.EqualFold(host, \"localhost\") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {\n\t\treturn fmt.Errorf(\"ssrf: host %q is blocked (loopback)\", host)\n\t}\n\n\t// Resolve ALL addresses the hostname currently maps to.  A hostname that\n\t// returns even one blocked address is rejected (defense-in-depth against\n\t// split-horizon / DNS rebinding scenarios where the pre-flight check and\n\t// the dial see different answers).\n\taddrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ssrf: DNS resolution failed for %q: %w\", host, err)\n\t}\n\tif len(addrs) == 0 {\n\t\treturn fmt.Errorf(\"ssrf: host %q resolved to no addresses\", host)\n\t}\n\n\tfor _, ia := range addrs {\n\t\ta, ok := netip.AddrFromSlice(ia.IP)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"ssrf: could not parse resolved IP %v for host %q\", ia.IP, host)\n\t\t}\n\t\ta = a.Unmap() // normalise ::ffff:x.x.x.x → x.x.x.x\n\t\tif err := checkAddr(a, host, port, cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateHostInput rejects URL/userinfo-shaped values before they reach DNS\n// or an error formatter. IP literals (including zoned IPv6) are handled by\n// netip.ParseAddr and may contain colons or a zone identifier.","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L205-L241","documentation":"Pre-flight check in ssrf.ValidateURL: the hostname resolved without error but LookupIPAddr returned zero addresses. Treated as a hard failure because there is nothing to range-check — an empty answer must not be allowed to slip past the SSRF policy.","triggerScenarios":"Calling ssrf.ValidateURL for a name whose DNS answer contains no A/AAAA records (e.g. only MX/NS records exist, or a stale empty RRset), or a resolver returning an empty NOERROR answer.","commonSituations":"Zone exists but the A record was deleted; hostname taken from an email domain (MX-only); CDN/registrar placeholder zones with no address records.","solutions":["Verify the name actually has A/AAAA records: `dig example.com A +short` should list addresses.","Fix the DNS record or use the correct service hostname.","If using a search-domain/suffix setup, use the FQDN with trailing dot if the bare short name resolves empty."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"addrs, err := net.DefaultResolver.LookupIPAddr(ctx, u.Hostname())\nif err == nil && len(addrs) == 0 {\n    return fmt.Errorf(\"hostname has no address records\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check that integration hostnames have A/AAAA records, not just a DNS zone.","Beware MX-only domains (email providers) being reused as API hosts."],"tags":["ssrf","dns","network","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}