zeroclaw-labs/zeroclaw · error

Cross-host redirects are blocked so DNS validation remains p

Error message

Cross-host redirects are blocked so DNS validation remains pinned

What it means

web_fetch pins its DNS/security validation to the host of the originally requested URL. When the HTTP client follows a redirect, validate_redirect_target re-parses the Location header and requires its host to equal the pinned host exactly (string comparison — 'www.example.com' and 'example.com.' are different). Any host change is refused so that a redirect cannot move the request to a host that never went through validation, which would defeat the DNS-pinning anti-rebinding design.

Source

Thrown at crates/zeroclaw-tools/src/web_fetch.rs:645

    }
}

fn proxy_conflicts_with_dns_pinning(config: &ProxyConfig) -> bool {
    (config.enabled && config.scope == ProxyScope::Environment)
        || (config.has_any_proxy_url() && config.should_apply_to_service("tool.web_fetch"))
}

fn validate_redirect_target(
    raw_url: &str,
    pinned_host: &str,
    allowed_domains: &[String],
    blocked_domains: &[String],
    allowed_private_hosts: &[String],
) -> anyhow::Result<()> {
    let redirect_url = reqwest::Url::parse(raw_url)
        .map_err(|e| anyhow::Error::msg(format!("Invalid URL format: {e}")))?;
    if redirect_url.host_str() != Some(pinned_host) {
        anyhow::bail!("Cross-host redirects are blocked so DNS validation remains pinned");
    }

    validate_target_url_with_dns_check(
        raw_url,
        allowed_domains,
        blocked_domains,
        allowed_private_hosts,
        "web_fetch",
        |_, _| Ok(()),
    )?;
    Ok(())
}

fn resolve_target_url(
    raw_url: &str,
    allowed_domains: &[String],
    blocked_domains: &[String],
    allowed_private_hosts: &[String],

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Resolve the final URL yourself (e.g. a HEAD/GET without redirect following, or a link-expander) and fetch that target directly — it will then be fully validated against allow/block domains on its own merits
  2. If the redirect is legitimate and constant, update the stored source URL to the destination host
  3. Do not loosen the same-host rule for convenience; it is the anti-rebinding boundary

Example fix

// before
fetch("https://bit.ly/3xyz123")  // redirects to example.org -> bails
// after
let final = resolve_without_following("https://bit.ly/3xyz123"); // "https://example.org/post"
fetch(final)  // validated + fetched on its own
Defensive patterns

Strategy: validation

Validate before calling

// Expand redirects yourself, then fetch the final URL directly
async fn final_url(u: &str) -> anyhow::Result<String> {
    let client = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?;
    let resp = client.head(u).send().await?;
    Ok(resp.headers().get("location")
        .and_then(|l| l.to_str().ok()).map(str::to_string).unwrap_or_else(|| u.into()))
}

Type guard

fn same_host(a: &str, b: &str) -> bool {
    match (reqwest::Url::parse(a), reqwest::Url::parse(b)) {
        (Ok(x), Ok(y)) => x.host_str().is_some() && x.host_str() == y.host_str(),
        _ => false,
    }
}

Try / catch

Err(e) if e.to_string().starts_with("Cross-host redirects are blocked") => {
    // resolve the redirect chain manually, then issue a fresh, fully-validated fetch of the final URL
}

Prevention

When it happens

Trigger: Short links (bit.ly, t.co), canonicalization redirects from www to apex or vice versa, CDN failovers, http-to-https redirects that also change hostname, and redirects from a pinned host to an attacker-chosen host (the case the guard exists for).

Common situations: Fetching news articles or shared links that redirect; operators enabling redirect following for convenience and then hitting legitimate infrastructural redirects; security reviews confirming redirect-based SSRF is contained (the cited tests pin exactly this).

Related errors


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