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
- Point the upstream at a public IP or hostname that resolves publicly.
- 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.
- 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).
- 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
- Keep AllowPrivateIPs false in production.
- Use DNS names (not literal IPs) for upstreams so the dial-time guard can re-check.
- Audit Balancer.Servers for private/literal IPs at config load.
- Document any deliberate opt-in to private IPs.
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
- ErrUpstreamHostBlocked
- %w: %q
- proxy: upstream scheme is not allowed
- proxy: parse upstream %q: %w
- %w: %s lookup failed: %w
AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11).
Data as JSON: /api/errors/cc5c5914265c7dc0.
Report an issue: GitHub.