gofiber/fiber · critical

ErrUpstreamHostBlocked

ErrUpstreamHostBlocked

Error message

proxy: upstream host resolves to a blocked address

What it means

validateUpstreamForBalancer rejects upstreams that are IP literals falling inside a blocked range when the SecurityPolicy has AllowPrivateIPs false (the default). Blocked ranges cover loopback (127/8), unspecified (0.0.0.0, ::), RFC 1918 private (10/8, 172.16/12, 192.168/16), link-local including the 169.254.169.254 cloud-metadata address, multicast, interface-local multicast, RFC 6598 CGNAT (100.64/10), and several IPv6 transition ranges. Key distinction: Balancer defers hostname DNS resolution to dial-time, so this construction-time block fires ONLY for IP literals — a hostname like "localhost" passes construction and is blocked per-request by the SSRF dial guard.

Source

Thrown at middleware/proxy/proxy.go:41

// Balancer creates a load balancer among multiple upstream servers
func Balancer(config ...Config) fiber.Handler {
	// Set default config
	cfg := configDefault(config...)
	policy := resolvePolicy(cfg.SecurityPolicy)

	// Load balanced client
	lbc := &fasthttp.LBClient{}
	// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TLSConfig
	// will not be used if the client are set.
	if cfg.Client == nil {
		// Set timeout
		lbc.Timeout = cfg.Timeout
		// Validate each upstream against the configured policy and build
		// a HostClient per server.
		for _, server := range cfg.Servers {
			u, err := validateUpstreamForBalancer(server, policy)
			if err != nil {
				panic(err)
			}

			client := &fasthttp.HostClient{
				NoDefaultUserAgentHeader: true,
				DisablePathNormalizing:   true,
				Addr:                     u.Host,
				MaxConns:                 cfg.MaxConnsPerHost,

				ReadBufferSize:  cfg.ReadBufferSize,
				WriteBufferSize: cfg.WriteBufferSize,

				TLSConfig: secureTLSConfig(cfg.TLSConfig),

				DialDualStack: cfg.DialDualStack,

				MaxResponseBodySize: cfg.MaxResponseBodySize,
			}
			if u.Scheme == schemeHTTPS {

View on GitHub (pinned to a105acad6c)

Solutions

  1. If reaching a private/internal upstream is intentional and the SSRF surface is understood, opt in explicitly: clone proxy.DefaultSecurityPolicy(), set AllowPrivateIPs: true, and pass it via Config.SecurityPolicy.
  2. Use a public hostname or public IP for the upstream so it passes the blocklist.
  3. For local development, run the upstream on a non-loopback, non-RFC1918 address, or toggle AllowPrivateIPs only in dev builds.

Example fix

// before
app.Use(proxy.Balancer(proxy.Config{
    Servers: []string{"http://127.0.0.1:8080"},
}))

// after
policy := proxy.DefaultSecurityPolicy()
policy.AllowPrivateIPs = true // explicit, SSRF-risk-acknowledged
app.Use(proxy.Balancer(proxy.Config{
    Servers:       []string{"http://127.0.0.1:8080"},
    SecurityPolicy: &policy,
}))
Defensive patterns

Strategy: validation

Validate before calling

// returns true if an IP-literal upstream would be blocked by the proxy policy
func upstreamIPIsBlocked(raw string) bool {
    u, err := url.Parse(raw)
    if err == nil && !strings.Contains(raw, "://") {
        u, _ = url.Parse("http://" + raw)
    }
    if u == nil { return false }
    if ip := net.ParseIP(strings.Trim(u.Hostname(), "[]")); ip != nil {
        return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
            ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified()
    }
    return false // hostnames are resolved at dial-time, not here
}
// if upstreamIPIsBlocked(server) && !policy.AllowPrivateIPs { decide: allow or reject }

Type guard

func isLikelyPrivateUpstream(raw string) bool {
    u, err := url.Parse(raw)
    if err == nil && !strings.Contains(raw, "://") { u, _ = url.Parse("http://" + raw) }
    if u == nil { return false }
    ip := net.ParseIP(strings.Trim(u.Hostname(), "[]"))
    return ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast())
}

Prevention

When it happens

Trigger: proxy.Balancer(proxy.Config{Servers: []string{"http://127.0.0.1:8080"}}), or {"http://10.0.0.5"}, {"http://169.254.169.254"}, {"http://100.64.0.1"} with the default policy (AllowPrivateIPs: false).

Common situations: Local development pointing the proxy at 127.0.0.1; a cloud workload trying to reach an internal service on a private subnet; a misconfigured upstream inherited from a service mesh that exposes link-local addresses; forgetting that the secure-by-default policy blocks all internal ranges.

Related errors


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