{"record":{"id":"ba04a15eedda8312","repo":"JuliusBrussee/caveman","slug":"ssrf-host-q-is-blocked-loopback","errorCode":null,"errorMessage":"ssrf: host %q is blocked (loopback)","messagePattern":"ssrf: host %q is blocked \\(loopback\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/ssrf/ssrf.go","lineNumber":211,"sourceCode":"func ValidateHost(ctx context.Context, host string, cfg Config) error {\n\treturn validateHostPort(ctx, host, \"\", cfg)\n}\n\nfunc validateHostPort(ctx context.Context, host, port string, cfg Config) error {\n\tif err := validateHostInput(host); err != nil {\n\t\treturn err\n\t}\n\n\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)","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/ssrf/ssrf.go#L193-L229","documentation":"Pre-flight host check in ssrf.ValidateURL: the literal hostname 'localhost' is blocked unconditionally in managed mode, and in self-hosted mode it is blocked unless an explicit allowlist entry covers it. DNS is never consulted for this name — the block is by string match (case-insensitive).","triggerScenarios":"Calling ssrf.ValidateURL with host 'localhost' (any port) while ManagedMode is true; or with ManagedMode false but no matching AllowList entry for 'localhost:port'.","commonSituations":"Developer leaves a local test webhook (http://localhost:8000/hook — note http is also blocked in managed mode) in a config that ships to the managed environment; self-hosted operator pointing an integration at a local service without allowlisting it.","solutions":["Point the integration at the real external hostname instead of localhost.","Self-hosted only: add 'localhost:<port>' (or the concrete loopback IP) to cfg.AllowList so the operator opt-in is explicit.","In managed mode there is no escape — remove localhost targets from tenant-supplied config."],"exampleFix":"// before\ncfg := ssrf.Config{ManagedMode: false}\nerr := ssrf.ValidateURL(ctx, \"https://localhost:8443/hook\", cfg) // blocked\n\n// after\ncfg := ssrf.Config{\n    ManagedMode: false,\n    AllowList:   []string{\"localhost:8443\"},\n}\nerr := ssrf.ValidateURL(ctx, \"https://localhost:8443/hook\", cfg)","handlingStrategy":"validation","validationCode":"u, _ := url.Parse(raw)\nif strings.EqualFold(u.Hostname(), \"localhost\") &&\n    !(cfg.ManagedMode == false && isInAllowList(u.Hostname(), u.Port(), cfg.AllowList)) {\n    return fmt.Errorf(\"localhost is not permitted; use the service's real hostname\")\n}","typeGuard":null,"tryCatchPattern":"if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {\n    if strings.Contains(err.Error(), \"blocked (loopback)\") {\n        // reject config save with guidance instead of retrying\n    }\n}","preventionTips":["Never save localhost URLs in tenant-facing config for managed deployments.","Self-hosted operators: keep explicit 'localhost:port' allowlist entries instead of widening mode."],"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"}