{"record":{"id":"5a3bc9044e2b0f82","repo":"zeroclaw-labs/zeroclaw","slug":"url-userinfo-is-not-allowed-5a3bc9","errorCode":null,"errorMessage":"URL userinfo is not allowed","messagePattern":"URL userinfo is not allowed","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-tools/src/text_browser.rs","lineNumber":233,"sourceCode":"    let url = 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(\"http://\") && !url.starts_with(\"https://\") {\n        anyhow::bail!(\"Only http:// and https:// URLs are allowed\");\n    }\n\n    let parsed = reqwest::Url::parse(url)\n        .map_err(|e| anyhow::Error::msg(format!(\"Invalid URL format: {e}\")))?;\n\n    if !parsed.username().is_empty() || parsed.password().is_some() {\n        anyhow::bail!(\"URL userinfo is not allowed\");\n    }\n\n    let host_str = parsed\n        .host_str()\n        .ok_or_else(|| anyhow::Error::msg(\"URL must include a host\"))?;\n\n    let bare_host = host_str.trim_start_matches('[').trim_end_matches(']');\n    let is_ipv6 = bare_host.parse::<std::net::Ipv6Addr>().is_ok();\n    let (host, display_host) = if is_ipv6 {\n        let bare = bare_host.parse::<std::net::Ipv6Addr>().unwrap().to_string();\n        (bare.clone(), format!(\"[{bare}]\"))\n    } else {\n        let h = host_str.to_lowercase();\n        (h.clone(), h)\n    };\n\n    // SSRF gate: deny by default for private/local hosts unless the operator\n    // explicitly listed them. Mirrors `browser`/`http_request`/`web_fetch`.","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/text_browser.rs#L215-L251","documentation":"After parsing the URL with reqwest, validate_text_browser_url rejects any URL carrying userinfo — a non-empty username or a password component (http://user:pass@host/). This blocks credentials embedded in URLs, which leak into logs, proxies, and error messages, and can also be used in parser-confusion attacks.","triggerScenarios":"Passing \"https://user:pass@example.com/\", \"http://token@example.com/\", or URLs built by concatenating credentials into the authority section.","commonSituations":"Copy-pasting authenticated links from browsers that embed basic-auth credentials; scripts migrating curl -u user:pass style auth into a URL instead of headers.","solutions":["Remove userinfo from the URL and send credentials through the appropriate header mechanism (e.g. an Authorization header on tools that support it, or secrets config)","URL-encode nothing here — the presence of '@' before the host is itself rejected","For public endpoints, just use the bare https://host/path form"],"exampleFix":"// before\n{\"url\":\"https://admin:s3cret@example.com/report\"}\n// after\n{\"url\":\"https://example.com/report\"} // auth handled via headers/secrets elsewhere","handlingStrategy":"validation","validationCode":"let parsed = reqwest::Url::parse(url)?;\nif !parsed.username().is_empty() || parsed.password().is_some() { /* reject early */ }","typeGuard":"fn url_has_userinfo(u: &str) -> bool {\n    reqwest::Url::parse(u).map(|p| !p.username().is_empty() || p.password().is_some()).unwrap_or(true)\n}","tryCatchPattern":"Err(e) if e.to_string().ends_with(\"userinfo is not allowed\") => {\n    // strip credentials from the URL, move them to header/secrets handling, retry\n}","preventionTips":["Never build URLs by embedding credentials; pass auth via headers or secret references","Scan logs for '://' followed by '@' patterns as a leak check","Teach users that browsers hide userinfo — check the pasted string when debugging"],"tags":["url","userinfo","credentials","validation","security"],"backgroundTag":"url-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}