{"record":{"id":"d944958c718eb8f0","repo":"gofiber/fiber","slug":"errupstreamhostblocked-d94495","errorCode":"ErrUpstreamHostBlocked","errorMessage":"proxy: upstream host resolves to a blocked address","messagePattern":"proxy: upstream host resolves to a blocked address","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"middleware/proxy/proxy.go","lineNumber":41,"sourceCode":"// Balancer creates a load balancer among multiple upstream servers\nfunc Balancer(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\tpolicy := resolvePolicy(cfg.SecurityPolicy)\n\n\t// Load balanced client\n\tlbc := &fasthttp.LBClient{}\n\t// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TLSConfig\n\t// will not be used if the client are set.\n\tif cfg.Client == nil {\n\t\t// Set timeout\n\t\tlbc.Timeout = cfg.Timeout\n\t\t// Validate each upstream against the configured policy and build\n\t\t// a HostClient per server.\n\t\tfor _, server := range cfg.Servers {\n\t\t\tu, err := validateUpstreamForBalancer(server, policy)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tclient := &fasthttp.HostClient{\n\t\t\t\tNoDefaultUserAgentHeader: true,\n\t\t\t\tDisablePathNormalizing:   true,\n\t\t\t\tAddr:                     u.Host,\n\t\t\t\tMaxConns:                 cfg.MaxConnsPerHost,\n\n\t\t\t\tReadBufferSize:  cfg.ReadBufferSize,\n\t\t\t\tWriteBufferSize: cfg.WriteBufferSize,\n\n\t\t\t\tTLSConfig: secureTLSConfig(cfg.TLSConfig),\n\n\t\t\t\tDialDualStack: cfg.DialDualStack,\n\n\t\t\t\tMaxResponseBodySize: cfg.MaxResponseBodySize,\n\t\t\t}\n\t\t\tif u.Scheme == schemeHTTPS {","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/gofiber/fiber/blob/a105acad6c1e4576a77f01e02973f67e962bb58d/middleware/proxy/proxy.go#L23-L59","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: []string{\"http://127.0.0.1:8080\"},\n}))\n\n// after\npolicy := proxy.DefaultSecurityPolicy()\npolicy.AllowPrivateIPs = true // explicit, SSRF-risk-acknowledged\napp.Use(proxy.Balancer(proxy.Config{\n    Servers:       []string{\"http://127.0.0.1:8080\"},\n    SecurityPolicy: &policy,\n}))","handlingStrategy":"validation","validationCode":"// returns true if an IP-literal upstream would be blocked by the proxy policy\nfunc upstreamIPIsBlocked(raw string) bool {\n    u, err := url.Parse(raw)\n    if err == nil && !strings.Contains(raw, \"://\") {\n        u, _ = url.Parse(\"http://\" + raw)\n    }\n    if u == nil { return false }\n    if ip := net.ParseIP(strings.Trim(u.Hostname(), \"[]\")); ip != nil {\n        return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||\n            ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified()\n    }\n    return false // hostnames are resolved at dial-time, not here\n}\n// if upstreamIPIsBlocked(server) && !policy.AllowPrivateIPs { decide: allow or reject }","typeGuard":"func isLikelyPrivateUpstream(raw string) bool {\n    u, err := url.Parse(raw)\n    if err == nil && !strings.Contains(raw, \"://\") { u, _ = url.Parse(\"http://\" + raw) }\n    if u == nil { return false }\n    ip := net.ParseIP(strings.Trim(u.Hostname(), \"[]\"))\n    return ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast())\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["proxy","ssrf","security","config","startup","panic"],"backgroundTag":null,"analyzedSha":"a105acad6c1e4576a77f01e02973f67e962bb58d","analyzedAt":"2026-08-11T17:33:26.942Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}