JuliusBrussee/caveman · error

ssrf: destination %s (for host %q) is in blocked range %s

Error message

ssrf: destination %s (for host %q) is in blocked range %s

What it means

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.

Source

Thrown at shared/platform/ssrf/ssrf.go:278

func checkAddr(addr netip.Addr, host, port string, cfg Config) error {
	// Strip any IPv6 zone identifier (e.g. fe80::1%eth0) before range checks:
	// netip.Prefix.Contains returns false for ANY zoned address, so without this
	// a zoned literal like "fe80::1%eth0" or "::1%lo0" would evade every blocked
	// prefix and defeat the loopback/link-local guard.
	addr = addr.WithZone("").Unmap()

	for _, p := range loopbackPrefixes {
		if p.Contains(addr) {
			// Managed mode blocks loopback absolutely — allowing tenants to
			// route through 127.x or ::1 would trivially reach local-only
			// services. Self-hosted mode opts back in only via an explicit
			// allowlist entry: the original host, the IP literal, or
			// "localhost" (dial time only ever sees the resolved IP).
			if !cfg.ManagedMode &&
				(isInAllowList(host, port, cfg.AllowList) || isInAllowList(addr.String(), port, cfg.AllowList) || isInAllowList("localhost", port, cfg.AllowList)) {
				return nil
			}
			return fmt.Errorf("ssrf: destination %s (for host %q) is in blocked range %s", addr, host, p)
		}
	}

	for _, p := range selfHostedSyntheticPrefixes {
		if p.Contains(addr) {
			if !cfg.ManagedMode {
				return nil
			}
			return fmt.Errorf("ssrf: destination %s (for host %q) is in blocked range %s", addr, host, p)
		}
	}

	for _, p := range blockedPrefixes {
		if p.Contains(addr) {
			// These ranges (link-local/metadata, ULA outside the narrow local-TUN
			// exception, multicast, unspecified, documentation) are absolutely
			// blocked — no allowlist escape in any mode.
			return fmt.Errorf("ssrf: destination %s (for host %q) is in blocked range %s", addr, host, p)

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Self-hosted: add the exact target ('127.0.0.1:9000', 'host:9000', or 'localhost:9000') to cfg.AllowList.
  2. Otherwise point at the service's real non-loopback address/hostname.
  3. Managed mode: no escape exists by design — remove loopback targets from configuration.

Example fix

// before
cfg := ssrf.Config{ManagedMode: false}
client := ssrf.NewHTTPClient(cfg) // dial to 127.0.0.1:9000 blocked

// after
cfg := ssrf.Config{ManagedMode: false, AllowList: []string{"127.0.0.1:9000"}}
client := ssrf.NewHTTPClient(cfg)
Defensive patterns

Strategy: validation

Validate before calling

host, port := u.Hostname(), u.Port()
ips, _ := net.DefaultResolver.LookupIPAddr(ctx, host)
for _, ip := range ips {
    a, _ := netip.AddrFromSlice(ip)
    if a.IsLoopback() && !isInAllowList(host, port, cfg.AllowList) &&
        !isInAllowList(a.String(), port, cfg.AllowList) && !isInAllowList("localhost", port, cfg.AllowList) {
        return fmt.Errorf("target resolves to loopback; allowlist it or use a real host")
    }
}

Try / catch

if err := ssrf.ValidateURL(ctx, raw, cfg); err != nil {
    if strings.Contains(err.Error(), "blocked range") && strings.Contains(err.Error(), "127.") {
        // loopback hit: fix target or extend AllowList (self-hosted only)
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: /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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/cba88bb0b65dc185. Report an issue: GitHub.