diff --git a/Cargo.lock b/Cargo.lock index c4045d23b..2e3928104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,15 +113,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arc-swap" version = "1.9.1" @@ -1086,12 +1077,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1190,17 +1175,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "digest" version = "0.10.7" @@ -1565,9 +1539,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -4447,6 +4423,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typemap_rev" version = "0.3.0" @@ -5178,19 +5160,18 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "9.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6" dependencies = [ - "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", + "getrandom 0.4.3", "indexmap", "memchr", - "thiserror 2.0.18", - "zopfli", + "nix", + "typed-path", + "windows-sys 0.61.2", ] [[package]] @@ -5199,18 +5180,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zune-core" version = "0.5.1" diff --git a/crates/openab-core/Cargo.toml b/crates/openab-core/Cargo.toml index 1ede398f1..51aca393f 100644 --- a/crates/openab-core/Cargo.toml +++ b/crates/openab-core/Cargo.toml @@ -41,7 +41,7 @@ tempfile = "3.27.0" aws-sdk-secretsmanager = { version = "1", optional = true } aws-sdk-s3 = { version = "1", optional = true } aws-config = { version = "1", optional = true } -zip = { version = "2", default-features = false, features = ["deflate"], optional = true } +zip = { version = "9.1.2", default-features = false, features = ["deflate-flate2"], optional = true } flate2 = { version = "1", optional = true } tar = { version = "0.4.45", default-features = false, optional = true } aws-sigv4 = { version = "1", optional = true } diff --git a/crates/openab-core/src/pre_seed.rs b/crates/openab-core/src/pre_seed.rs index 2bea072c0..0f1c6ee6f 100644 --- a/crates/openab-core/src/pre_seed.rs +++ b/crates/openab-core/src/pre_seed.rs @@ -225,6 +225,12 @@ fn extract_zip_budgeted( } let mut file = archive.by_index(i)?; + // zip 9.x `enclosed_name()` strips a leading root and accepts absolute + // entry names; zip 2.x rejected them. Keep rejecting so an archive with + // absolute paths fails closed, as before the 2.x -> 9.x bump. + if matches!(file.name_raw().first(), Some(b'/' | b'\\')) { + anyhow::bail!("hooks.pre_seed: invalid zip entry name at index {i}"); + } let name = file.enclosed_name().ok_or_else(|| { anyhow::anyhow!("hooks.pre_seed: invalid zip entry name at index {i}") })?; @@ -663,6 +669,48 @@ mod tests { ); } + #[test] + fn extract_rejects_path_traversal() { + use std::io::Write; + // Pins the path-safety contract across the zip 2.x -> 9.x bump: every + // escaping or absolute entry name is rejected and nothing is written. + fn zip_with(name: &str) -> Vec { + let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + writer + .start_file(name, zip::write::SimpleFileOptions::default()) + .unwrap(); + writer.write_all(b"pwned").unwrap(); + writer.finish().unwrap().into_inner() + } + let deadline = Instant::now() + std::time::Duration::from_secs(60); + + for evil in [ + "../escape.txt", + "sub/../../escape.txt", + "/abs_escape.txt", + "\\abs_escape.txt", + ] { + let root = tempfile::tempdir().unwrap(); + let dest = root.path().join("dest"); + std::fs::create_dir_all(&dest).unwrap(); + + let err = extract_zip_with_limits(&zip_with(evil), &dest, deadline).unwrap_err(); + assert!( + err.to_string().contains("invalid zip entry name"), + "{evil}: expected rejection, got {err}" + ); + assert!( + !root.path().join("escape.txt").exists(), + "{evil}: escaped dest" + ); + assert_eq!( + std::fs::read_dir(&dest).unwrap().count(), + 0, + "{evil}: dest not empty" + ); + } + } + #[test] fn extract_rejects_exceeding_file_count() { use std::io::Write;