gofiber/fiber · critical · ErrUpstreamHostBlocked

%w: %s

Error message

%w: %s

What it means

validateUpstreamForBalancer statically checks IP-literal upstream hosts at Balancer construction time. If the host is a literal IP in a blocked range (loopback, RFC1918, link-local, multicast, unspecified, CGNAT 100.64/10, or blocked IPv6 transition ranges) and AllowPrivateIPs is false, this error fires. Hostname resolution is deferred to the dial-time guard, so this only catches literal IPs.

Source

Thrown at middleware/proxy/security.go:351

}

// validateUpstreamForBalancer validates a statically configured Balancer
// upstream. It enforces the scheme allowlist and rejects IP-literal hosts
// in blocked ranges, but defers hostname resolution to the SSRF-guarded
// dialer (see newSSRFDialer). Deferring DNS keeps a transient resolver
// failure at startup from panicking the application (e.g. crash loops in
// container orchestrators) and re-checks the resolved IP on every dial,
// which also defeats DNS-rebinding.
func validateUpstreamForBalancer(raw string, policy SecurityPolicy) (*url.URL, error) {
	u, err := parseUpstreamScheme(raw, policy)
	if err != nil {
		return nil, err
	}
	if policy.AllowPrivateIPs {
		return u, nil
	}
	if ip := net.ParseIP(trimBrackets(u.Hostname())); ip != nil && isBlockedIP(ip) {
		return nil, fmt.Errorf("%w: %s", ErrUpstreamHostBlocked, ip)
	}
	return u, nil
}

// schemeAllowed reports whether scheme is on the allowlist. An empty
// allowlist falls back to the secure defaults.
func schemeAllowed(scheme string, allowed []string) bool {
	if scheme == "" {
		return false
	}
	if len(allowed) == 0 {
		allowed = defaultAllowedSchemes
	}
	for _, s := range allowed {
		if utils.EqualFold(s, scheme) {
			return true
		}
	}

View on GitHub (pinned to a105acad6c)

Solutions

  1. Point the upstream at a public IP or hostname that resolves publicly.
  2. If the target is legitimately internal (e.g. sidecar in the same pod), explicitly opt in via SecurityPolicy{AllowPrivateIPs: true} and document the SSRF exposure this introduces.
  3. Use a DNS name rather than a literal IP so the dial-time guard can re-check the resolved address on every connect (defeats rebinding).
  4. Audit the Balancer.Servers slice at config load to catch blocked literals before runtime.

Example fix

// before: literal private IP blocked at startup
balancer.Servers = []string{"http://10.0.0.5:8080"}

// after: explicit opt-in for an internal sidecar
WithSecurityPolicy(DefaultSecurityPolicy()) // AllowPrivateIPs: true
Defensive patterns

Strategy: validation

Validate before calling

func validateBalancerUpstream(raw string, policy proxy.SecurityPolicy) error {
  u, err := url.Parse(strings.TrimSpace(raw))
  if err != nil { return err }
  if ip := net.ParseIP(strings.Trim(u.Hostname(), "[]")); ip != nil && !policy.AllowPrivateIPs {
    // replicate isBlockedIP or rely on validateUpstreamForBalancer at construction
  }
  return nil
}

Prevention

When it happens

Trigger: Balancer config includes an upstream like http://127.0.0.1, http://10.0.0.5, http://169.254.169.254 (cloud metadata), http://100.64.0.1 (CGNAT), or an IPv6 literal in a transition range. Triggered once at startup/config-reload.

Common situations: Local dev pointing a Balancer at localhost; misconfigured service discovery returning a private IP; an attempt to reach a cloud metadata endpoint through the proxy; IPv6 6to4/Teredo/NAT64-local addresses.

Related errors


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/cc5c5914265c7dc0. Report an issue: GitHub.