{"record":{"id":"5856e8b1cbf6ee63","repo":"JuliusBrussee/caveman","slug":"ssrf-dns-resolution-failed-for-q-w","errorCode":null,"errorMessage":"ssrf: DNS resolution failed for %q: %w","messagePattern":"ssrf: DNS resolution failed for %q: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":220,"sourceCode":"\t// If host is an IP literal, check it directly without a DNS round-trip.\n\tif addr, err := netip.ParseAddr(host); err == nil {\n\t\treturn checkAddr(addr, host, port, cfg)\n\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","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L202-L238","documentation":"Pre-flight DNS failure in ssrf.ValidateURL: net.DefaultResolver.LookupIPAddr returned an error for the hostname. The original resolver error is wrapped with %w, so the cause (NXDOMAIN, timeout, SERVFAIL) is visible in the chain.","triggerScenarios":"Calling ssrf.ValidateURL with a hostname that does not exist (NXDOMAIN), a resolver that times out, or a host with only a malformed/unresolvable record. IP-literal hosts skip this path entirely.","commonSituations":"Typo'd domains in webhook config; recently-expired or not-yet-propagated DNS records; broken /etc/resolv.conf or blocked DNS egress in containers; split-horizon DNS where the name only resolves inside another network.","solutions":["dig/host the name from the same container/host to confirm it resolves: `dig +short example.com`.","Fix the typo or use the canonical hostname from the provider docs.","If the resolver is the problem, repair container DNS (resolv.conf, cluster DNS) or add the host to a local zone — do not bypass the SSRF check.","Retry once for transient resolver timeouts before surfacing the error to the user."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if _, err := net.DefaultResolver.LookupIPAddr(ctx, u.Hostname()); err != nil {\n    return fmt.Errorf(\"host does not resolve: %w\", err) // fail before building the request\n}","typeGuard":"func isDNSFailure(err error) bool {\n    var dnsErr *net.DNSError\n    return errors.As(err, &dnsErr)\n}","tryCatchPattern":"err := ssrf.ValidateURL(ctx, raw, cfg)\nif err != nil {\n    var dnsErr *net.DNSError\n    if errors.As(err, &dnsErr) && dnsErr.IsTimeout && !dnsErr.IsNotFound {\n        // transient resolver issue: retry once\n    } else if dnsErr != nil && dnsErr.IsNotFound {\n        // permanent: bad hostname, surface to user\n    }\n}","preventionTips":["Validate hostnames at config save time so users see DNS errors immediately.","Distinguish NXDOMAIN (permanent) from timeouts (transient) before retrying."],"tags":["ssrf","dns","network","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}