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

  1. 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)
  2. URL-encode nothing here — the presence of '@' before the host is itself rejected
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/5a3bc9044e2b0f82. Report an issue: GitHub.