{"record":{"id":"7e6b3f460e79269d","repo":"zed-industries/zed","slug":"refusing-to-follow-redirect-to-non-http-s-url-ta","errorCode":null,"errorMessage":"refusing to follow redirect to non-HTTP(S) URL {target}","messagePattern":"refusing to follow redirect to non-HTTP\\(S\\) URL (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/agent/src/tools/fetch_tool.rs","lineNumber":99,"sourceCode":"        let normalized = normalize_url(url);\n\n        let mut response = http_client\n            .get(&normalized, AsyncBody::default(), false)\n            .await?;\n\n        let status = response.status();\n        if status.is_redirection() {\n            let location = response\n                .headers()\n                .get(\"location\")\n                .context(\"redirect response is missing a Location header\")?\n                .to_str()\n                .context(\"redirect response has an invalid Location header\")?;\n            let target = url::Url::parse(&normalized)\n                .with_context(|| format!(\"could not parse URL {normalized:?}\"))?\n                .join(location)\n                .with_context(|| format!(\"invalid redirect target {location:?}\"))?;\n            anyhow::ensure!(\n                matches!(target.scheme(), \"http\" | \"https\"),\n                \"refusing to follow redirect to non-HTTP(S) URL {target}\"\n            );\n            return Ok(FetchStep::Redirect(target.to_string()));\n        }\n\n        let mut body = Vec::new();\n        response\n            .body_mut()\n            .read_to_end(&mut body)\n            .await\n            .context(\"error reading response body\")?;\n\n        if status.is_client_error() {\n            let text = String::from_utf8_lossy(body.as_slice());\n            bail!(\"status error {}, response: {text:?}\", status.as_u16());\n        }\n","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/crates/agent/src/tools/fetch_tool.rs#L81-L117","documentation":"The fetch tool received a 3xx response whose Location header, resolved against the current URL, yields a scheme other than http/https (e.g. file://, ftp:, data:). As a defense against protocol smuggling and local-file exfiltration the tool refuses to follow the redirect and errors, naming the refused target URL.","triggerScenarios":"A fetched URL returns a redirect with `Location: file:///...`, `ftp://...`, or any non-HTTP(S) URI; the parsed-and-joined target fails the http/https scheme check.","commonSituations":"Adversarial or compromised sites redirecting to local files; misconfigured servers emitting ftp:// links; test fixtures with exotic Location headers; model-supplied URLs that hop protocols.","solutions":["If the resource is genuinely available over http(s), fetch that final URL directly and fix the server's redirect.","If you own the server, emit absolute http(s) Location headers.","Never relax the scheme check — it is a security boundary; only http and https are allowed.","Treat occurrences on external sites as suspicious (possible exfiltration attempt) and stop following the chain."],"exampleFix":"# before: server responds with\nHTTP/3 301 Location: ftp://example.com/file\n\n# after: server responds with\nHTTP/3 301 Location: https://example.com/file","handlingStrategy":"validation","validationCode":"// Resolve redirects yourself and verify every hop before fetching:\nlet mut current = url.to_string();\nfor _ in 0..MAX_REDIRECTS {\n    let response = issue_request(&current).await?;\n    if !response.status().is_redirection() { break; }\n    let location = header_str(response.headers(), \"location\").unwrap_or(\"\");\n    let next = url::Url::parse(&current)?.join(location)?;\n    ensure!(\n        matches!(next.scheme(), \"http\" | \"https\"),\n        \"refusing to follow redirect to non-HTTP(S) URL {next}\"\n    );\n    current = next.to_string();\n}","typeGuard":"fn is_http_url(url: &str) -> bool {\n    url::Url::parse(url)\n        .map(|parsed| matches!(parsed.scheme(), \"http\" | \"https\"))\n        .unwrap_or(false)\n}","tryCatchPattern":"match fetch_tool_run(url).await {\n    Err(err) if err.to_string().contains(\"non-HTTP(S) URL\") => {\n        // Redirect target refused by design — fetch an approved http(s) URL instead.\n        report_refused_redirect(err.to_string());\n    }\n    other => other,\n}","preventionTips":["Never instruct the agent to follow redirects to file:// or ftp:// URLs.","Fix your own servers to emit absolute http(s) Location headers.","Treat this error on external sites as a possible SSRF/exfiltration attempt and stop."],"tags":["fetch","http","security","redirect","ssrf"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}