{"record":{"id":"ffa98dce2eff40bd","repo":"zeroclaw-labs/zeroclaw","slug":"blocked-local-private-host-host-ffa98d","errorCode":null,"errorMessage":"Blocked local/private host: {host}","messagePattern":"Blocked local/private host: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_open.rs","lineNumber":70,"sourceCode":"        }\n\n        if !(url.starts_with(\"https://\") || url.starts_with(\"http://\")) {\n            anyhow::bail!(\"Only http:// or https:// URLs are allowed\");\n        }\n\n        if self.allowed_domains.is_empty() && self.allowed_private_hosts.is_empty() {\n            anyhow::bail!(\n                \"Browser tool is enabled but no allowed_domains are configured. Add [browser].allowed_domains in config.toml\"\n            );\n        }\n\n        let host = extract_host(url)?;\n        let private_host = domain_guard::is_private_or_local_host(&host);\n        let private_host_allowed = private_host\n            && domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);\n\n        if private_host && !private_host_allowed {\n            anyhow::bail!(\"Blocked local/private host: {host}\");\n        }\n\n        if private_host_allowed {\n            return Ok(url.to_string());\n        }\n\n        if !domain_guard::host_matches_allowlist(&host, &self.allowed_domains) {\n            anyhow::bail!(\"Host '{host}' is not in browser.allowed_domains\");\n        }\n\n        Ok(url.to_string())\n    }\n}\n\n#[async_trait]\nimpl Tool for BrowserOpenTool {\n    fn name(&self) -> &str {\n        \"browser_open\"","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_open.rs#L52-L88","documentation":"browser_open classifies the URL's host via domain_guard::is_private_or_local_host (loopback, private LAN ranges, link-local, localhost-style names). Private hosts are blocked by default as an SSRF guard; the only escape hatch is listing the host in allowed_private_hosts, in which case validation accepts the URL early.","triggerScenarios":"Opening http://localhost:8080, http://127.0.0.1:9090, http://192.168.1.10/ui, or http://myserver.local when the host is not present in [browser].allowed_private_hosts.","commonSituations":"Developers wanting the agent to open a local admin UI or dev server; home-lab hostnames under .local; cloud metadata-style addresses (169.254.169.254) that the guard intentionally blocks as SSRF.","solutions":["Add the exact host to [browser].allowed_private_hosts in config.toml (e.g. \"localhost\", \"192.168.1.10\") and restart","Or use the service's public hostname, which then goes through the normal allowed_domains check","Do not work around by adding broad private ranges unless you accept the SSRF exposure"],"exampleFix":"# config.toml — before\n[browser]\nallowed_domains = [\"example.com\"]\n\n# after\n[browser]\nallowed_domains = [\"example.com\"]\nallowed_private_hosts = [\"localhost\", \"127.0.0.1\"]","handlingStrategy":"validation","validationCode":"fn looks_private(host: &str) -> bool {\n    host == \"localhost\"\n        || host.starts_with(\"127.\")\n        || host.starts_with(\"10.\")\n        || host.starts_with(\"192.168.\")\n        || host.starts_with(\"169.254.\")\n        || host.ends_with(\".local\")\n}\nif looks_private(&host) && !allowed_private_hosts.contains(&host.to_string()) {\n    return Err(format!(\"private host '{host}' blocked; add it to allowed_private_hosts\"));\n}","typeGuard":null,"tryCatchPattern":"match open_tool.execute(args).await {\n    Ok(res) if res.success => { /* ... */ }\n    Ok(res) => {\n        if res.error.as_deref().unwrap_or_default().contains(\"Blocked local/private host\") {\n            // SSRF guard: add the exact host to allowed_private_hosts or use a public hostname\n        }\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["List only the exact local hosts needed in allowed_private_hosts","Prefer public hostnames for services the agent must open","Never add broad private ranges (e.g. whole 10.0.0.0/8) casually"],"tags":["browser-open","ssrf","private-network","security","allowlist"],"backgroundTag":"private-network-access-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}