diff --git a/Cargo.lock b/Cargo.lock index 34376751d..881f427cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12152,14 +12152,15 @@ dependencies = [ [[package]] name = "zip" -version = "9.1.3" +version = "9.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb" +checksum = "00fb4744e923442ecadfd3f85f51a5d24e03bf43d53813b81ebb4c91d02baa62" dependencies = [ "crc32fast", "flate2", "getrandom 0.4.3", "indexmap 2.14.2", + "libc", "memchr", "nix 0.31.3", "typed-path", diff --git a/Cargo.toml b/Cargo.toml index bf6a10847..a22b3ddae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ trash = "5" blake3 = "1" dunce = "1" dirs = "6" -zip = { version = "9", default-features = false, features = ["deflate"] } +zip = { version = "9.2.3", default-features = false, features = ["deflate"] } # search, index, language grep-searcher = "0.1" diff --git a/crates/ide-run/src/tomcat.rs b/crates/ide-run/src/tomcat.rs index 7895c8f8b..75360f96c 100644 --- a/crates/ide-run/src/tomcat.rs +++ b/crates/ide-run/src/tomcat.rs @@ -1228,6 +1228,48 @@ mod tests { } zip.finish().unwrap(); } + #[test] + fn explodes_wars_with_multibyte_path_components() { + let tmp = tempfile::tempdir().unwrap(); + let war = tmp.path().join("app.war"); + let entries = [ + ("bdms300-front/dist/img/ZC机柜.jpg", "reported case"), + ("img/A机柜.jpg", "one ASCII byte"), + ("img/😀.jpg", "four-byte character"), + ("img/abé.jpg", "two-byte character"), + ("ZC机柜/index.jsp", "multibyte directory"), + ("img/普通.jpg", "character boundary"), + ]; + archive(&war, &entries); + for resources_only in [false, true] { + let destination = tmp.path().join(format!("out-{resources_only}")); + explode(&war, &destination, resources_only).unwrap(); + for (name, text) in entries { + assert_eq!(fs::read_to_string(destination.join(name)).unwrap(), text, "{name}"); + } + } + } + + #[test] + fn rejects_war_path_traversal() { + let tmp = tempfile::tempdir().unwrap(); + let war = tmp.path().join("app.war"); + archive(&war, &[("../escape.txt", "outside")]); + assert!(explode(&war, &tmp.path().join("out"), false).is_err()); + assert!(!tmp.path().join("escape.txt").exists()); + } + + #[cfg(windows)] + #[test] + fn rejects_war_reserved_device_paths() { + let tmp = tempfile::tempdir().unwrap(); + let war = tmp.path().join("app.war"); + for name in ["CON.txt", "NUL", "COM1.txt", "LPT¹", "img/aux.jpg"] { + archive(&war, &[(name, "device")]); + assert!(explode(&war, &tmp.path().join("out"), false).is_err(), "{name}"); + } + } + #[test] fn keeps_the_apps_own_context_descriptor() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-tools/src/archive.rs b/crates/ide-tools/src/archive.rs index 5afb35572..19ce0bd14 100644 --- a/crates/ide-tools/src/archive.rs +++ b/crates/ide-tools/src/archive.rs @@ -443,6 +443,24 @@ mod tests { } } + #[test] + fn extracts_zip_with_multibyte_path_components() { + let tmp = tempfile::tempdir().unwrap(); + let archive = tmp.path().join("tool.zip"); + let entries: &[(&str, &[u8], u32)] = &[ + ("bdms300-front/dist/img/ZC机柜.jpg", b"reported case", 0o644), + ("A机柜/file", b"directory", 0o644), + ("abé.txt", b"two-byte character", 0o644), + ("😀.txt", b"four-byte character", 0o644), + ]; + zip_file(&archive, entries); + let dest = tmp.path().join("out"); + extract(&archive, ArchiveKind::Zip, &dest, "x", &CancellationToken::new()).unwrap(); + for (name, data, _) in entries { + assert_eq!(fs::read(dest.join(name)).unwrap(), *data, "{name}"); + } + } + #[test] fn extracts_zip_with_modes_and_nested_dirs() { let tmp = tempfile::tempdir().unwrap();