diff --git a/crates/fetch/README.md b/crates/fetch/README.md index 60ac1d329..8bad4003b 100644 --- a/crates/fetch/README.md +++ b/crates/fetch/README.md @@ -2,6 +2,6 @@ Native model downloads in pure Rust (L0, Apache-2.0): HTTP/1.1 over std sockets and rustls with the RustCrypto provider, without ring, aws-lc, C or assembly build dependencies. The wasm32 crate is empty. -Call `download` with bounded file specifications and ordered mirrors, or `download_url` for an exact catalog URL (including its query). Each file has a local filename independent of the URL, a size cap and optional pinned size/SHA-256. Transfers resume pinned `.part` files, verify before renaming, report progress and check cancellation during reads. Run on a worker thread. No proxy support; DNS resolution uses the platform resolver. Tokens are off by default and never forwarded to a different redirect host. +Call `download` with bounded file specifications and ordered mirrors, or `download_url` for an exact catalog URL (including its query). Each file has a local filename independent of the URL, a size cap and optional pinned size/SHA-256. Transfers resume pinned `.part` files, verify before renaming, report progress and check cancellation during reads. Run on a worker thread. No proxy support; DNS resolution uses the platform resolver. Tokens are off by default and never forwarded to a different redirect host. A redirect from https to http is refused. Tests use local synthetic HTTP servers for redirects, resume, truncation, cancellation, stalls, hashes, size limits and hostile specifications; they make no internet requests. diff --git a/crates/fetch/src/http.rs b/crates/fetch/src/http.rs index e900850ac..c2b5bee1e 100644 --- a/crates/fetch/src/http.rs +++ b/crates/fetch/src/http.rs @@ -110,7 +110,11 @@ impl Url { pub fn join(&self, location: &str) -> Result { let l = location.trim(); if l.starts_with("http://") || l.starts_with("https://") { - return Url::parse(l); + let url = Url::parse(l)?; + if self.tls && !url.tls { + return Err(HttpError::BadUrl("redirect from https to http refused".into())); + } + return Ok(url); } // another scheme (`ftp:`, `file:`, `data:`…) if let Some((scheme, _)) = l.split_once(':') @@ -541,6 +545,11 @@ mod tests { assert_eq!(u.join("other").unwrap().path, "/x/y/other"); assert_eq!(u.join("//c.example/p").unwrap().host, "c.example"); assert!(u.join("ftp://nope").is_err()); + // no downgrade from https to http, however the redirect is written + assert!(u.join("http://a.example/z").is_err()); + let plain = Url::parse("http://127.0.0.1:8080/a").unwrap(); + assert!(!plain.join("http://127.0.0.1:9/b").unwrap().tls); + assert!(plain.join("https://b.example/z").unwrap().tls); } #[test]