zeroclaw-labs/zeroclaw · error · anyhow::Error

Blocked local/private host: {host}

Error message

Blocked local/private host: {host}

What it means

Thrown by HttpRequestTool::validate_url_policy (crates/zeroclaw-tools/src/http_request.rs:159) when domain_guard::is_private_or_local_host(host) is true (localhost, loopback, RFC1918 ranges, ULA/link-local IPv6, etc.) AND the host is not listed in allowed_private_hosts AND the allow_private_hosts flag is false. This is the configurable half of the SSRF guard: private hosts are blocked by default but can be explicitly permitted, unlike the metadata/link-local checks.

Source

Thrown at crates/zeroclaw-tools/src/http_request.rs:159

        if let Ok(ip) = host.parse::<IpAddr>() {
            if domain_guard::is_known_cloud_metadata_endpoint(ip) {
                anyhow::bail!("Blocked cloud metadata host: {host}");
            }
            if domain_guard::is_cloud_metadata_ip(ip) {
                anyhow::bail!(
                    "Blocked link-local host: {host}; 169.254.0.0/16 is blocked unconditionally \
                     because cloud metadata services are hosted in that range"
                );
            }
        }
        let port = extract_port(url)?;

        let private_host = domain_guard::is_private_or_local_host(&host);
        let private_host_explicitly_allowed = private_host
            && domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);

        if private_host && !private_host_explicitly_allowed && !self.allow_private_hosts {
            anyhow::bail!("Blocked local/private host: {host}");
        }

        if !private_host_explicitly_allowed
            && !domain_guard::host_matches_allowlist(&host, &self.allowed_domains)
        {
            anyhow::bail!("Host '{host}' is not in http_request.allowed_domains");
        }

        let private_resolution_allowed = self.allow_private_hosts
            || domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);

        let canonical_url = if host.parse::<IpAddr>().is_ok() {
            url.to_string()
        } else {
            let mut parsed = reqwest::Url::parse(url)
                .map_err(|e| anyhow::Error::msg(format!("Invalid URL format: {e}")))?;
            parsed
                .set_host(Some(&host))

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Add the specific host to [http_request] allowed_private_hosts (e.g. allowed_private_hosts = ["192.168.1.5", "myservice.internal"]); this also satisfies the allowed_domains check for that host.
  2. If broad internal access is intended, set allow_private_hosts = true in [http_request] (metadata/link-local IPs remain blocked).
  3. For public SaaS endpoints, verify you are not accidentally hitting a private mirror or internal proxy hostname.

Example fix

# before
[http_request]
allowed_domains = ["localhost"]  # does not help: private check fires first

# after
[http_request]
allowed_domains = ["example.com"]
allowed_private_hosts = ["localhost", "127.0.0.1"]
Defensive patterns

Strategy: validation

Validate before calling

fn private_host_allowed(host: &str, allowed_private: &[String], allow_all: bool) -> bool {
    if allow_all { return true; }
    let h = host.trim_end_matches('.').to_lowercase();
    allowed_private.iter().any(|a| h == *a || h.ends_with(&format!(".{a}")))
}

Try / catch

let result = tool.execute(args).await?;
if let Some(err) = &result.error {
    if err.contains("Blocked local/private host") {
        // suggest adding the host to allowed_private_hosts (not allowed_domains)
    }
}

Prevention

When it happens

Trigger: url = "https://localhost:8080" or "http://127.0.0.1:9000" with default config; "https://192.168.1.5" or "https://10.0.0.12:8443" (RFC1918 literals); "http://[::1]:8080/" (IPv6 loopback); internal DNS names like "https://myservice.internal" that resolve-to/are-classified private. Note: allowed_domains = ["*"] does NOT bypass this (private blocking runs first).

Common situations: Local development against a dev server on localhost while reusing production config; homelab/LAN service integrations; internal API gateways on RFC1918 space; Docker setups where the tool container reaches another container by its private IP.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/04da72d46c65c324. Report an issue: GitHub.