JuliusBrussee/caveman · error
ssrf: host %q is blocked (loopback)
Error message
ssrf: host %q is blocked (loopback)
What it means
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).
Source
Thrown at shared/platform/ssrf/ssrf.go:211
func ValidateHost(ctx context.Context, host string, cfg Config) error {
return validateHostPort(ctx, host, "", cfg)
}
func validateHostPort(ctx context.Context, host, port string, cfg Config) error {
if err := validateHostInput(host); err != nil {
return err
}
// If host is an IP literal, check it directly without a DNS round-trip.
if addr, err := netip.ParseAddr(host); err == nil {
return checkAddr(addr, host, port, cfg)
}
// "localhost" is explicitly blocked regardless of what DNS says — unless a
// self-hosted operator allowlisted it (resolution still runs, so every
// resolved address is range-checked below like any other).
if strings.EqualFold(host, "localhost") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {
return fmt.Errorf("ssrf: host %q is blocked (loopback)", host)
}
// Resolve ALL addresses the hostname currently maps to. A hostname that
// returns even one blocked address is rejected (defense-in-depth against
// split-horizon / DNS rebinding scenarios where the pre-flight check and
// the dial see different answers).
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return fmt.Errorf("ssrf: DNS resolution failed for %q: %w", host, err)
}
if len(addrs) == 0 {
return fmt.Errorf("ssrf: host %q resolved to no addresses", host)
}
for _, ia := range addrs {
a, ok := netip.AddrFromSlice(ia.IP)
if !ok {
return fmt.Errorf("ssrf: could not parse resolved IP %v for host %q", ia.IP, host)View on GitHub (pinned to 27d5a3981a)
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.
Example fix
// before
cfg := ssrf.Config{ManagedMode: false}
err := ssrf.ValidateURL(ctx, "https://localhost:8443/hook", cfg) // blocked
// after
cfg := ssrf.Config{
ManagedMode: false,
AllowList: []string{"localhost:8443"},
}
err := ssrf.ValidateURL(ctx, "https://localhost:8443/hook", cfg) Defensive patterns
Strategy: validation
Validate before calling
u, _ := url.Parse(raw)
if strings.EqualFold(u.Hostname(), "localhost") &&
!(cfg.ManagedMode == false && isInAllowList(u.Hostname(), u.Port(), cfg.AllowList)) {
return fmt.Errorf("localhost is not permitted; use the service's real hostname")
} Try / catch
if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {
if strings.Contains(err.Error(), "blocked (loopback)") {
// reject config save with guidance instead of retrying
}
} Prevention
- Never save localhost URLs in tenant-facing config for managed deployments.
- Self-hosted operators: keep explicit 'localhost:port' allowlist entries instead of widening mode.
When it happens
Trigger: Calling ssrf.ValidateURL with host 'localhost' (any port) while ManagedMode is true; or with ManagedMode false but no matching AllowList entry for 'localhost:port'.
Common situations: 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.
Related errors
- ssrf: destination %s (for host %q) is in blocked range %s
- ssrf: scheme %q not permitted (managed mode requires https)
- ssrf: destination %s (for host %q) is a private address bloc
- ssrf: credentials embedded in URL are forbidden
- ssrf: DNS resolution failed for %q: %w
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/ba04a15eedda8312.
Report an issue: GitHub.