{"record":{"id":"04da72d46c65c324","repo":"zeroclaw-labs/zeroclaw","slug":"blocked-local-private-host-host-04da72","errorCode":null,"errorMessage":"Blocked local/private host: {host}","messagePattern":"Blocked local/private host: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/http_request.rs","lineNumber":159,"sourceCode":"        if let Ok(ip) = host.parse::<IpAddr>() {\n            if domain_guard::is_known_cloud_metadata_endpoint(ip) {\n                anyhow::bail!(\"Blocked cloud metadata host: {host}\");\n            }\n            if domain_guard::is_cloud_metadata_ip(ip) {\n                anyhow::bail!(\n                    \"Blocked link-local host: {host}; 169.254.0.0/16 is blocked unconditionally \\\n                     because cloud metadata services are hosted in that range\"\n                );\n            }\n        }\n        let port = extract_port(url)?;\n\n        let private_host = domain_guard::is_private_or_local_host(&host);\n        let private_host_explicitly_allowed = private_host\n            && domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);\n\n        if private_host && !private_host_explicitly_allowed && !self.allow_private_hosts {\n            anyhow::bail!(\"Blocked local/private host: {host}\");\n        }\n\n        if !private_host_explicitly_allowed\n            && !domain_guard::host_matches_allowlist(&host, &self.allowed_domains)\n        {\n            anyhow::bail!(\"Host '{host}' is not in http_request.allowed_domains\");\n        }\n\n        let private_resolution_allowed = self.allow_private_hosts\n            || domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);\n\n        let canonical_url = if host.parse::<IpAddr>().is_ok() {\n            url.to_string()\n        } else {\n            let mut parsed = reqwest::Url::parse(url)\n                .map_err(|e| anyhow::Error::msg(format!(\"Invalid URL format: {e}\")))?;\n            parsed\n                .set_host(Some(&host))","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/http_request.rs#L141-L177","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","If broad internal access is intended, set allow_private_hosts = true in [http_request] (metadata/link-local IPs remain blocked).","For public SaaS endpoints, verify you are not accidentally hitting a private mirror or internal proxy hostname."],"exampleFix":"# before\n[http_request]\nallowed_domains = [\"localhost\"]  # does not help: private check fires first\n\n# after\n[http_request]\nallowed_domains = [\"example.com\"]\nallowed_private_hosts = [\"localhost\", \"127.0.0.1\"]","handlingStrategy":"validation","validationCode":"fn private_host_allowed(host: &str, allowed_private: &[String], allow_all: bool) -> bool {\n    if allow_all { return true; }\n    let h = host.trim_end_matches('.').to_lowercase();\n    allowed_private.iter().any(|a| h == *a || h.ends_with(&format!(\".{a}\")))\n}","typeGuard":null,"tryCatchPattern":"let result = tool.execute(args).await?;\nif let Some(err) = &result.error {\n    if err.contains(\"Blocked local/private host\") {\n        // suggest adding the host to allowed_private_hosts (not allowed_domains)\n    }\n}","preventionTips":["Keep a per-environment [http_request] allowed_private_hosts list for internal services instead of flipping allow_private_hosts globally.","Remember allowed_domains wildcards never unlock private hosts; only the private-host config does.","Use hostnames, not raw RFC1918 IPs, so allowlist entries stay stable."],"tags":["http","security","ssrf","private-network","configuration","zeroclaw"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}