{"record":{"id":"d01450ae74830882","repo":"zeroclaw-labs/zeroclaw","slug":"cross-host-redirects-are-blocked-so-dns-validation","errorCode":null,"errorMessage":"Cross-host redirects are blocked so DNS validation remains pinned","messagePattern":"Cross-host redirects are blocked so DNS validation remains pinned","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/web_fetch.rs","lineNumber":645,"sourceCode":"    }\n}\n\nfn proxy_conflicts_with_dns_pinning(config: &ProxyConfig) -> bool {\n    (config.enabled && config.scope == ProxyScope::Environment)\n        || (config.has_any_proxy_url() && config.should_apply_to_service(\"tool.web_fetch\"))\n}\n\nfn validate_redirect_target(\n    raw_url: &str,\n    pinned_host: &str,\n    allowed_domains: &[String],\n    blocked_domains: &[String],\n    allowed_private_hosts: &[String],\n) -> anyhow::Result<()> {\n    let redirect_url = reqwest::Url::parse(raw_url)\n        .map_err(|e| anyhow::Error::msg(format!(\"Invalid URL format: {e}\")))?;\n    if redirect_url.host_str() != Some(pinned_host) {\n        anyhow::bail!(\"Cross-host redirects are blocked so DNS validation remains pinned\");\n    }\n\n    validate_target_url_with_dns_check(\n        raw_url,\n        allowed_domains,\n        blocked_domains,\n        allowed_private_hosts,\n        \"web_fetch\",\n        |_, _| Ok(()),\n    )?;\n    Ok(())\n}\n\nfn resolve_target_url(\n    raw_url: &str,\n    allowed_domains: &[String],\n    blocked_domains: &[String],\n    allowed_private_hosts: &[String],","sourceCodeStart":627,"sourceCodeEnd":663,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/web_fetch.rs#L627-L663","documentation":"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.","triggerScenarios":"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).","commonSituations":"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).","solutions":["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","If the redirect is legitimate and constant, update the stored source URL to the destination host","Do not loosen the same-host rule for convenience; it is the anti-rebinding boundary"],"exampleFix":"// before\nfetch(\"https://bit.ly/3xyz123\")  // redirects to example.org -> bails\n// after\nlet final = resolve_without_following(\"https://bit.ly/3xyz123\"); // \"https://example.org/post\"\nfetch(final)  // validated + fetched on its own","handlingStrategy":"validation","validationCode":"// Expand redirects yourself, then fetch the final URL directly\nasync fn final_url(u: &str) -> anyhow::Result<String> {\n    let client = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?;\n    let resp = client.head(u).send().await?;\n    Ok(resp.headers().get(\"location\")\n        .and_then(|l| l.to_str().ok()).map(str::to_string).unwrap_or_else(|| u.into()))\n}","typeGuard":"fn same_host(a: &str, b: &str) -> bool {\n    match (reqwest::Url::parse(a), reqwest::Url::parse(b)) {\n        (Ok(x), Ok(y)) => x.host_str().is_some() && x.host_str() == y.host_str(),\n        _ => false,\n    }\n}","tryCatchPattern":"Err(e) if e.to_string().starts_with(\"Cross-host redirects are blocked\") => {\n    // resolve the redirect chain manually, then issue a fresh, fully-validated fetch of the final URL\n}","preventionTips":["Store final destination URLs rather than short links in long-lived data","Expand links at ingestion time so redirects never happen during security-sensitive fetches","Remember the host match is exact-string: normalize www/trailing-dot variants before comparing","Treat a redirect to an unexpected host in untrusted content as a red flag, not an inconvenience"],"tags":["redirect","ssrf","security","http","web-fetch"],"backgroundTag":"cross-host-redirect-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}