zeroclaw-labs/zeroclaw · error · anyhow::Error

Generated image URL userinfo is not allowed

Error message

Generated image URL userinfo is not allowed

What it means

parse_public_https_url rejects any generated image URL containing userinfo (a username or password before the host, as in https://user:pass@host/path). Embedded credentials can confuse host parsing and leak secrets into logs, so the SSRF validation pipeline refuses them outright. The same rule exists in the http_request tool's extract_host.

Source

Thrown at crates/zeroclaw-tools/src/image_gen.rs:35

struct ValidatedImageTarget {
    url: reqwest::Url,
    host: String,
    resolved_addrs: Vec<SocketAddr>,
}

fn parse_public_https_url(raw_url: &str) -> anyhow::Result<(reqwest::Url, String, u16)> {
    let raw_url = raw_url.trim();
    if raw_url.is_empty() || raw_url.chars().any(char::is_whitespace) {
        anyhow::bail!("Generated image URL must be a non-empty URL without whitespace");
    }

    let mut url = reqwest::Url::parse(raw_url).context("Invalid generated image URL")?;
    if url.scheme() != "https" {
        anyhow::bail!("Generated image URL must use HTTPS");
    }
    if !url.username().is_empty() || url.password().is_some() {
        anyhow::bail!("Generated image URL userinfo is not allowed");
    }

    let request_host = url
        .host_str()
        .ok_or_else(|| anyhow::Error::msg("Generated image URL must include a host"))?;
    if request_host.ends_with('.') {
        anyhow::bail!("Generated image URL host must not end with a dot");
    }
    let host = domain_guard::normalize_domain(request_host)
        .ok_or_else(|| anyhow::Error::msg("Generated image URL host is invalid"))?;
    let ip_literal = host.parse::<IpAddr>().ok();
    if domain_guard::is_private_or_local_host(&host) {
        anyhow::bail!("Generated image URL targets a local or non-global host");
    }
    if ip_literal.is_some_and(domain_guard::is_cloud_metadata_ip) {
        anyhow::bail!("Generated image URL targets a cloud metadata host");
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Reconfigure the storage/CDN to issue credential-free URLs (signed query parameters or short-lived links) and pass any secret via headers if the producer supports it.
  2. Strip the userinfo component from the URL at the source that produces it, so the delivered link is bare host + path.
  3. Never work around this by relaying credentials another way inside the image URL; the guard exists to keep secrets out of the fetch path.

Example fix

# before
https://user:pass@cdn.example.com/files/img.png   # rejected

# after
https://cdn.example.com/files/img.png?token=...   # accepted
Defensive patterns

Strategy: validation

Validate before calling

fn without_userinfo(raw: &str) -> bool {
    reqwest::Url::parse(raw).map(|u| u.username().is_empty() && u.password().is_none()).unwrap_or(false)
}

Type guard

fn is_bare_host_url(u: &reqwest::Url) -> bool { u.username().is_empty() && u.password().is_none() }

Try / catch

match validate_image_target(url, nat64).await {
    Err(e) if e.to_string().contains("userinfo is not allowed") => {
        // credentials embedded in the link: move auth to signed query params or headers at the producer
    }
    other => other,
}

Prevention

When it happens

Trigger: A fal.ai response or custom storage configuration returns a URL like 'https://access-key@cdn.example.com/img.png' or 'https://user:pass@storage.internal/img.png' — typically signed URLs that put tokens in the userinfo position or proxies that embed basic-auth credentials in the link.

Common situations: Storage backends or CDNs that generate authenticated links with credentials embedded in the URL, shared-secret SAS-style tokens placed before the @ sign, and copy-pasted URLs from curl examples that include -u style credentials.

Related errors


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