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
- 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.
- Use a public hostname or public IP for the upstream so it passes the blocklist.
- 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
- Make AllowPrivateIPs a deliberate, documented decision per environment — never silently inherit the default for internal-facing proxies.
- In CI, run a test that constructs your Balancer against the real upstream list to catch blocked-IP entries before deploy.
- Keep internal and public upstreams in separate handlers so a private entry cannot poison a public-facing balancer.
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
- ErrUpstreamHostInvalid
- ErrUpstreamSchemeNotAllowed
- proxy: upstream scheme is not allowed
- ErrUpstreamHostBlocked
- %w: %q
AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11).
Data as JSON: /api/errors/d944958c718eb8f0.
Report an issue: GitHub.