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
- 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.
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
- 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.
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
- ssrf: host %q is blocked (loopback)
- 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/cba88bb0b65dc185.
Report an issue: GitHub.