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
- 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.
- Strip the userinfo component from the URL at the source that produces it, so the delivered link is bare host + path.
- 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
- Configure storage to sign URLs with query parameters instead of userinfo credentials.
- Scan provider URLs for '@' before the host in your validation layer.
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
- Generated image URL must use HTTPS
- URL must include a valid host
- Generated image URL must be a non-empty URL without whitespa
- Generated image URL host must not end with a dot
- Generated image URL targets a local or non-global host
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/53e8c0632bff927d.
Report an issue: GitHub.