{"record":{"id":"635c78eb5a94eb19","repo":"JuliusBrussee/caveman","slug":"ssrf-could-not-parse-resolved-ip-v-for-host-q","errorCode":null,"errorMessage":"ssrf: could not parse resolved IP %v for host %q","messagePattern":"ssrf: could not parse resolved IP (.+?) for host %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":229,"sourceCode":"\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.\nfunc validateHostInput(host string) error {\n\tif host == \"\" {\n\t\treturn errors.New(\"ssrf: invalid host\")\n\t}\n\tif _, err := netip.ParseAddr(host); err == nil {\n\t\treturn nil","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L211-L247","documentation":"Defensive parse failure in ssrf.ValidateURL: the resolver returned an IP that netip.AddrFromSlice could not convert. In practice this is near-impossible (LookupIPAddr yields 4/16-byte slices), so seeing it indicates resolver corruption or a pathological environment rather than a user mistake.","triggerScenarios":"A DNS response handing back a malformed address that still parses into a non-4/non-16 byte slice; essentially only reachable with a broken or hostile resolver in front of the process.","commonSituations":"Custom/captive DNS servers returning non-standard records; exotic test doubles stubbing LookupIPAddr with invalid data. Production users effectively never see this.","solutions":["Check what the resolver actually returns for the host (`dig example.com A`, `dig example.com AAAA`).","Point the process at a trustworthy recursive resolver (system/default) instead of the odd one in front of it.","If this appears in tests, fix the DNS stub to return valid net.IP values."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {\n    if strings.Contains(err.Error(), \"could not parse resolved IP\") {\n        // environment/resolver fault, not user input: alert ops\n    }\n}","preventionTips":["Use the system default resolver; avoid hand-rolled DNS responders in the request path.","In tests, stub resolvers with valid 4/16-byte IPs only."],"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"}