gofiber/fiber · critical · ErrUpstreamHostBlocked

%w: %s -> %s

Error message

%w: %s -> %s

What it means

validateHostForSSRF iterates every resolved address and blocks the request if ANY one is in a blocked range — even if other answers are public. This mixed-answer rejection is intentional: it mitigates DNS-rebinding where a resolver returns a public IP for validation and a private IP at connect time. The message shows host -> blocked-IP.

Source

Thrown at middleware/proxy/security.go:409

	if ip := net.ParseIP(host); ip != nil {
		if isBlockedIP(ip) {
			return fmt.Errorf("%w: %s", ErrUpstreamHostBlocked, ip)
		}
		return nil
	}
	// Bound the lookup so a slow resolver cannot stall the caller.
	ctx, cancel := context.WithTimeout(context.Background(), dnsLookupTimeout)
	defer cancel()
	addrs, err := dnsResolver.Load().LookupIPAddr(ctx, host)
	if err != nil {
		return fmt.Errorf("%w: %s lookup failed: %w", ErrUpstreamHostBlocked, host, err)
	}
	if len(addrs) == 0 {
		return fmt.Errorf("%w: %s has no addresses", ErrUpstreamHostBlocked, host)
	}
	for _, addr := range addrs {
		if isBlockedIP(addr.IP) {
			return fmt.Errorf("%w: %s -> %s", ErrUpstreamHostBlocked, host, addr.IP)
		}
	}
	return nil
}

// newSSRFDialer returns a fasthttp DialFunc that resolves the target host
// (with a bounded timeout), rejects the connection if any resolved
// address falls in a blocked range, and then dials a validated address.
// Performing the check at dial time — rather than only up front — defeats
// DNS-rebinding attacks (the check/use gap) where a resolver returns a
// public address during validation and a private one at connect time. It
// is only installed when the active policy disallows private IPs.
//
//nolint:revive // dialDualStack mirrors fasthttp.HostClient.DialDualStack
func newSSRFDialer(dialDualStack bool) fasthttp.DialFunc {
	dialer := &net.Dialer{Timeout: dnsLookupTimeout}
	return func(addr string) (net.Conn, error) {
		host, port, err := net.SplitHostPort(addr)

View on GitHub (pinned to a105acad6c)

Solutions

  1. Use a hostname whose DNS answers are all public IPs if AllowPrivateIPs is false.
  2. If the target is an approved internal service, set SecurityPolicy.AllowPrivateIPs: true via WithSecurityPolicy and document the SSRF acceptance.
  3. For user-supplied URLs, run your own DNS validation and reject mixed-answer or private-containing responses before calling proxy.Do.
  4. Pin to a specific validated IP if your architecture allows (the dial-time guard will still re-validate).
  5. Audit which hostnames are expected to resolve to private ranges and ensure they go through an opt-in policy.

Example fix

// before: hostname mixes public + private, blocked
WithSecurityPolicy(DefaultSecurityPolicy()) // AllowPrivateIPs: false
proxy.Do(ctx, "http://internal.svc")

// after: deliberate opt-in for approved internal target
WithSecurityPolicy(SecurityPolicy{AllowPrivateIPs: true})
proxy.Do(ctx, "http://internal.svc")
Defensive patterns

Strategy: validation

Validate before calling

// Reject hostnames that resolve to any blocked address.
func allPublic(host string) error {
  ips, err := net.LookupIP(host); if err != nil { return err }
  for _, ip := range ips { if isBlocked(ip) { return fmt.Errorf("%s -> %s blocked", host, ip) } }
  return nil
}

Prevention

When it happens

Trigger: A hostname resolves to a mix of public and private IPs (classic rebinding setup); a hostname that legitimately has both a public and a private view (split-horizon DNS seen by the same resolver); an internal hostname whose only records are private; a hostname resolving to 127.0.0.1, 169.254.x.x, or a CGNAT address.

Common situations: Adversarial DNS configured to flip answers; an internal service whose DNS legitimately returns RFC1918 addresses; cloud-provider internal hostnames; IPv6 transition-range addresses (6to4, Teredo, NAT64-local).

Related errors


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