{"record":{"id":"950532a69f9fbcf4","repo":"zeroclaw-labs/zeroclaw","slug":"only-http-or-https-urls-are-allowed","errorCode":null,"errorMessage":"Only http:// or https:// URLs are allowed","messagePattern":"Only http:// or https:// URLs are allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_open.rs","lineNumber":55,"sourceCode":"                allowed_private_hosts,\n                \"browser.allowed_private_hosts\",\n            )?,\n        })\n    }\n\n    fn validate_url(&self, raw_url: &str) -> anyhow::Result<String> {\n        let url = raw_url.trim();\n\n        if url.is_empty() {\n            anyhow::bail!(\"URL cannot be empty\");\n        }\n\n        if url.chars().any(char::is_whitespace) {\n            anyhow::bail!(\"URL cannot contain whitespace\");\n        }\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 {","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_open.rs#L37-L73","documentation":"browser_open only accepts URLs that literally start with https:// or http:// — a case-sensitive, byte-exact prefix check after trimming. There is no scheme parsing or defaulting: protocol-relative URLs (//example.com), uppercase schemes (HTTPS://), and bare hostnames all fail here, before any allowlist logic runs.","triggerScenarios":"url=\"example.com\", url=\"//example.com\", or url=\"HTTPS://example.com\" — the starts_with comparison never matches these.","commonSituations":"Users passing bare domains the way they type them into a browser address bar; markdown links using protocol-relative forms; code that uppercases URLs for logging and then reuses the uppercased copy.","solutions":["Always prefix the scheme in lowercase: https://example.com","Normalize scheme and host to lowercase before calling","When accepting human-typed input, default a missing scheme to https:// at your call site"],"exampleFix":"// before\n{\"url\": \"example.com\"}\n// after\n{\"url\": \"https://example.com\"}","handlingStrategy":"validation","validationCode":"let u = raw_url.trim();\nif !(u.starts_with(\"https://\") || u.starts_with(\"http://\")) {\n    return Err(\"URL must start with http:// or https:// (lowercase)\".into());\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(\"Only http:// or https://\") {\n            // add a lowercase scheme prefix and retry\n        }\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Normalize scheme and host to lowercase before calling","Default missing schemes to https:// at the call site for human input","Avoid protocol-relative //host URLs in agent tooling"],"tags":["browser-open","url","scheme","validation","case-sensitive"],"backgroundTag":"url-scheme-not-allowed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}