zeroclaw-labs/zeroclaw · warning · anyhow::Error
URL userinfo is not allowed
Error message
URL userinfo is not allowed
What it means
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.
Source
Thrown at crates/zeroclaw-tools/src/text_browser.rs:233
let url = url.trim();
if url.is_empty() {
anyhow::bail!("URL cannot be empty");
}
if url.chars().any(char::is_whitespace) {
anyhow::bail!("URL cannot contain whitespace");
}
if !url.starts_with("http://") && !url.starts_with("https://") {
anyhow::bail!("Only http:// and https:// URLs are allowed");
}
let parsed = reqwest::Url::parse(url)
.map_err(|e| anyhow::Error::msg(format!("Invalid URL format: {e}")))?;
if !parsed.username().is_empty() || parsed.password().is_some() {
anyhow::bail!("URL userinfo is not allowed");
}
let host_str = parsed
.host_str()
.ok_or_else(|| anyhow::Error::msg("URL must include a host"))?;
let bare_host = host_str.trim_start_matches('[').trim_end_matches(']');
let is_ipv6 = bare_host.parse::<std::net::Ipv6Addr>().is_ok();
let (host, display_host) = if is_ipv6 {
let bare = bare_host.parse::<std::net::Ipv6Addr>().unwrap().to_string();
(bare.clone(), format!("[{bare}]"))
} else {
let h = host_str.to_lowercase();
(h.clone(), h)
};
// SSRF gate: deny by default for private/local hosts unless the operator
// explicitly listed them. Mirrors `browser`/`http_request`/`web_fetch`.View on GitHub (pinned to 88bb9c8533)
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
Example fix
// before
{"url":"https://admin:s3cret@example.com/report"}
// after
{"url":"https://example.com/report"} // auth handled via headers/secrets elsewhere Defensive patterns
Strategy: validation
Validate before calling
let parsed = reqwest::Url::parse(url)?;
if !parsed.username().is_empty() || parsed.password().is_some() { /* reject early */ } Type guard
fn url_has_userinfo(u: &str) -> bool {
reqwest::Url::parse(u).map(|p| !p.username().is_empty() || p.password().is_some()).unwrap_or(true)
} Try / catch
Err(e) if e.to_string().ends_with("userinfo is not allowed") => {
// strip credentials from the URL, move them to header/secrets handling, retry
} Prevention
- 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
When it happens
Trigger: Passing "https://user:pass@example.com/", "http://token@example.com/", or URLs built by concatenating credentials into the authority section.
Common situations: 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.
Related errors
- unsupported URL scheme: {}
- Generated image URL userinfo is not allowed
- Lark/Feishu marker target uses a disallowed scheme
- Edge TTS binary_path must be a bare command name without pat
- refusing non-HTTPS attachment URL: {url}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/5a3bc9044e2b0f82.
Report an issue: GitHub.