JuliusBrussee/caveman · error
ssrf: host is blocked (loopback)
Error message
ssrf: host %q is blocked (loopback)
What it means
ValidateURLNoResolve blocks any URL whose host is the literal name 'localhost' (case-insensitive), because resolving it always targets the loopback interface — a classic SSRF vector. The check only passes if the host is explicitly in the configured allow list and the caller is not in managed mode. No DNS resolution is needed, hence the error surfaces before any network activity.
Solutions
- If the endpoint is genuinely local and intended, add the host:port to the ssrf config AllowList explicitly.
- Use the machine's assigned hostname or 127.0.0.1 per your policy (note: loopback IPs are blocked separately by checkAddr) — the supported path is an explicit allow-list entry.
- Replace localhost with the real upstream service URL; localhost targets in production config usually indicate a leftover dev setting.
- If you operate a proxy/gateway, point the client at the gateway hostname rather than a loopback service.
Example fix
// before
cfg := ssrf.DefaultConfig()
if err := ssrf.ValidateURLNoResolve("http://localhost:9090/hook", cfg); err != nil {
// ssrf: host "localhost" is blocked (loopback)
}
// after
cfg := ssrf.DefaultConfig()
cfg.AllowList = append(cfg.AllowList, "localhost:9090") // explicit, reviewed opt-in
if err := ssrf.ValidateURLNoResolve("http://localhost:9090/hook", cfg); err != nil { ... } Defensive patterns
Strategy: validation
Validate before calling
u, err := url.Parse(target)
if err == nil && strings.EqualFold(u.Hostname(), "localhost") {
return fmt.Errorf("refusing to send requests to localhost")
} Type guard
func isLoopbackHost(h string) bool {
return strings.EqualFold(h, "localhost")
} Try / catch
if err := ssrf.ValidateURLNoResolve(target, cfg); err != nil {
if strings.Contains(err.Error(), "blocked (loopback)") {
return fmt.Errorf("endpoint %q targets loopback and is not allow-listed: %w", target, err)
}
return err
} Prevention
- Never accept user-supplied webhook URLs without running the ssrf validator first.
- Keep the allow list minimal, explicit (host:port), and reviewed in code review.
- Sweep config for localhost URLs before production deploys.
- Validate scheme/host/port in one place so all egress paths share the same policy.
When it happens
Trigger: Calling ValidateURLNoResolve (directly or via ValidateUpstreamEndpoint) with a URL like http://localhost:8080, http://LOCALHOST, or http://LocalHost:port, where 'localhost' is not present in cfg.AllowList (or managed mode is enabled and the allow-list escape does not apply).
Common situations: User-supplied webhook/callback URLs pointing at localhost to probe the server itself; developer config pointing integrations at local services then deploying to production; SSRF probing attempts using localhost as the first payload tried.
Related errors
- githubapp: base_url rejected by SSRF guard
- githubapp: request path escaped configured host
- provider upstream URL must not include userinfo
- provider upstream URL path rejected
- ssrf: credentials embedded in URL are forbidden
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/ba04a15eedda8312.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/ssrf/ssrf.go:201
// the proxy's job. Behind a corporate proxy the process frequently has no
// outbound DNS at all, so resolving here fails the request before the proxy is
// ever consulted (#1001); and even where it resolves, the answer describes the
// client's view, not the proxy's. This is the same policy the proxied dial hook
// applies: host syntax, the localhost block, and range checks on IP literals.
// Hostname policy beyond that belongs to the proxy's ACL.
func ValidateURLNoResolve(raw string, cfg Config) error {
host, port, err := parseValidatedURL(raw, cfg)
if err != nil {
return err
}
if err := validateHostInput(host); err != nil {
return err
}
if addr, parseErr := netip.ParseAddr(host); parseErr == nil {
return checkAddr(addr, host, port, cfg)
}
if strings.EqualFold(host, "localhost") && !(!cfg.ManagedMode && isInAllowList(host, port, cfg.AllowList)) {
return fmt.Errorf("ssrf: host %q is blocked (loopback)", host)
}
return nil
}
// parseValidatedURL applies the URL-shape policy — scheme, embedded
// credentials, managed-mode port — and returns the host/port to check.
func parseValidatedURL(raw string, cfg Config) (host, port string, err error) {
u, parseErr := url.Parse(raw)
if parseErr != nil {
// net/url.Error includes the raw URL (and may therefore include
// credentials or query secrets). Keep this error field-only and stable.
return "", "", errors.New("ssrf: invalid URL")
}
if u.Scheme != "https" && !(u.Scheme == "http" && !cfg.ManagedMode) {
return "", "", fmt.Errorf("ssrf: scheme %q not permitted (managed mode requires https)", u.Scheme)
}
if u.User != nil {
return "", "", fmt.Errorf("ssrf: credentials embedded in URL are forbidden")View on GitHub (pinned to 3ee70a1026)