{"record":{"id":"cba88bb0b65dc185","repo":"JuliusBrussee/caveman","slug":"ssrf-destination-s-for-host-q-is-in-blocked-r","errorCode":null,"errorMessage":"ssrf: destination %s (for host %q) is in blocked range %s","messagePattern":"ssrf: destination (.+?) \\(for host %q\\) is in blocked range (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":278,"sourceCode":"func checkAddr(addr netip.Addr, host, port string, cfg Config) error {\n\t// Strip any IPv6 zone identifier (e.g. fe80::1%eth0) before range checks:\n\t// netip.Prefix.Contains returns false for ANY zoned address, so without this\n\t// a zoned literal like \"fe80::1%eth0\" or \"::1%lo0\" would evade every blocked\n\t// prefix and defeat the loopback/link-local guard.\n\taddr = addr.WithZone(\"\").Unmap()\n\n\tfor _, p := range loopbackPrefixes {\n\t\tif p.Contains(addr) {\n\t\t\t// Managed mode blocks loopback absolutely — allowing tenants to\n\t\t\t// route through 127.x or ::1 would trivially reach local-only\n\t\t\t// services. Self-hosted mode opts back in only via an explicit\n\t\t\t// allowlist entry: the original host, the IP literal, or\n\t\t\t// \"localhost\" (dial time only ever sees the resolved IP).\n\t\t\tif !cfg.ManagedMode &&\n\t\t\t\t(isInAllowList(host, port, cfg.AllowList) || isInAllowList(addr.String(), port, cfg.AllowList) || isInAllowList(\"localhost\", port, cfg.AllowList)) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is in blocked range %s\", addr, host, p)\n\t\t}\n\t}\n\n\tfor _, p := range selfHostedSyntheticPrefixes {\n\t\tif p.Contains(addr) {\n\t\t\tif !cfg.ManagedMode {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is in blocked range %s\", addr, host, p)\n\t\t}\n\t}\n\n\tfor _, p := range blockedPrefixes {\n\t\tif p.Contains(addr) {\n\t\t\t// These ranges (link-local/metadata, ULA outside the narrow local-TUN\n\t\t\t// exception, multicast, unspecified, documentation) are absolutely\n\t\t\t// blocked — no allowlist escape in any mode.\n\t\t\treturn fmt.Errorf(\"ssrf: destination %s (for host %q) is in blocked range %s\", addr, host, p)","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L260-L296","documentation":"checkAddr in ssrf: the resolved/dial address falls inside a loopback prefix (127.0.0.0/8 or ::1). Managed mode blocks loopback absolutely; self-hosted mode permits it only when the original host, the IP literal, or 'localhost' appears in the AllowList for that port. This is the range-level counterpart of the literal 'localhost' string check.","triggerScenarios":"A hostname that resolves to 127.x.x.x or ::1 (e.g. via /etc/hosts, a DNS rebinding setup, or an explicit 127.0.0.1 target) with no allowlist coverage, or any loopback target in managed mode.","commonSituations":"/etc/hosts pinning a public-looking name to 127.0.0.1; rebind attacks where DNS flips to loopback after validation; self-hosted operator targeting 127.0.0.1:9000 for a sidecar service without an allowlist entry.","solutions":["Self-hosted: add the exact target ('127.0.0.1:9000', 'host:9000', or 'localhost:9000') to cfg.AllowList.","Otherwise point at the service's real non-loopback address/hostname.","Managed mode: no escape exists by design — remove loopback targets from configuration."],"exampleFix":"// before\ncfg := ssrf.Config{ManagedMode: false}\nclient := ssrf.NewHTTPClient(cfg) // dial to 127.0.0.1:9000 blocked\n\n// after\ncfg := ssrf.Config{ManagedMode: false, AllowList: []string{\"127.0.0.1:9000\"}}\nclient := ssrf.NewHTTPClient(cfg)","handlingStrategy":"validation","validationCode":"host, port := u.Hostname(), u.Port()\nips, _ := net.DefaultResolver.LookupIPAddr(ctx, host)\nfor _, ip := range ips {\n    a, _ := netip.AddrFromSlice(ip)\n    if a.IsLoopback() && !isInAllowList(host, port, cfg.AllowList) &&\n        !isInAllowList(a.String(), port, cfg.AllowList) && !isInAllowList(\"localhost\", port, cfg.AllowList) {\n        return fmt.Errorf(\"target resolves to loopback; allowlist it or use a real host\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {\n    if strings.Contains(err.Error(), \"blocked range\") && strings.Contains(err.Error(), \"127.\") {\n        // loopback hit: fix target or extend AllowList (self-hosted only)\n    }\n}","preventionTips":["Resolve and inspect targets before storing them: loopback results usually mean /etc/hosts overrides or rebinding.","Scope allowlist entries to exact host:port or ip:port; never add whole loopback ranges."],"tags":["ssrf","loopback","network","security","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}