{"record":{"id":"5e486112542aea3f","repo":"googleapis/mcp-toolbox","slug":"redirect-to-blocked-ip-s-denied","errorCode":null,"errorMessage":"redirect to blocked IP %s denied","messagePattern":"redirect to blocked IP (.+?) denied","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sources/http/http.go","lineNumber":362,"sourceCode":"\n\tif r, ok := resolver.(*net.Resolver); ok {\n\t\tdialer.Resolver = r\n\t}\n\n\ttr.DialContext = dialer.DialContext\n\n\tclient := &http.Client{\n\t\tTimeout:   duration,\n\t\tTransport: tr,\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= 10 {\n\t\t\t\treturn fmt.Errorf(\"stopped after 10 redirects\")\n\t\t\t}\n\n\t\t\thostname := req.URL.Hostname()\n\t\t\tif ip := net.ParseIP(hostname); ip != nil {\n\t\t\t\tif guard.IsIPBlocked(ip) {\n\t\t\t\t\treturn fmt.Errorf(\"redirect to blocked IP %s denied\", ip)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\taddrs, err := resolver.LookupHost(req.Context(), hostname)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to resolve redirect host %s: %w\", hostname, err)\n\t\t\t}\n\n\t\t\tfor _, addr := range addrs {\n\t\t\t\tif ip := net.ParseIP(addr); ip != nil {\n\t\t\t\t\tif guard.IsIPBlocked(ip) {\n\t\t\t\t\t\treturn fmt.Errorf(\"redirect host %s resolves to blocked IP %s\", hostname, addr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil","sourceCodeStart":344,"sourceCodeEnd":380,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/http/http.go#L344-L380","documentation":"During redirect handling, if the next hop's hostname is a literal IP address, the source checks it against the SSRF guard (guard.IsIPBlocked). Blocked IPs include private/loopback/link-local ranges, so this error stops a request from being redirected into an internal network. It is a deliberate SSRF protection rejection.","triggerScenarios":"A 3xx response whose Location points to a URL with a literal IP host (e.g. http://169.254.169.254/ or http://127.0.0.1/) that guard.IsIPBlocked classifies as blocked.","commonSituations":"SSRF probing of cloud metadata endpoints via a redirect; a misconfigured server redirecting to localhost; user-supplied URL shorteners bouncing to internal addresses.","solutions":["Remove or fix the redirect target so the redirect chain stays on allowed public hosts.","If the IP is legitimately needed, allowlist it in the guard's blocked-IP policy configuration.","Verify no user-controlled input influences the initial URL that produces the redirect."],"exampleFix":"// before: original URL redirects to http://169.254.169.254/latest/meta-data/\n// after: point the tool at a public endpoint instead\ntoolArgs{URL: \"https://example.com/api\"}","handlingStrategy":"validation","validationCode":"function isPublicIPv4(ip) {\n  const parts = ip.split('.').map(Number);\n  if (parts.length !== 4 || parts.some(p => isNaN(p))) return false;\n  const [a, b] = parts;\n  const blocked = a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) ||\n    (a === 192 && b === 168) || (a === 169 && b === 254) || a === 0 || a >= 224;\n  return !blocked;\n}\n// reject URLs whose host is a blocked literal IP before calling the tool\nconst u = new URL(url); const host = u.hostname;\nif (/^[0-9.]+$/.test(host) && !isPublicIPv4(host)) throw new Error('literal private/metadata IP not allowed');","typeGuard":null,"tryCatchPattern":"try {\n  const result = await callHttpTool(url);\n} catch (err) {\n  if (String(err).includes('redirect to blocked IP')) {\n    console.error('Redirect target was a blocked/internal IP — remove SSRF-prone redirects from the server.');\n  } else throw err;\n}","preventionTips":["Never let user input control URLs that can trigger redirects toward internal/metadata addresses.","Prefer pinned public hostnames over raw IP URLs.","Review server redirect rules for references to localhost or link-local addresses."],"tags":["ssrf","security","redirect"],"backgroundTag":"ssrf-blocked-ip","analyzedSha":"8cc6e09de2ad7b8bffc77751799585a1401a48eb","analyzedAt":"2026-09-05T01:10:36.887Z","contentChangedAt":"2026-09-05T01:10:36.887Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}