diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..37c089ba --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +# Keep `cargo update` within the `rust-version` declared in Cargo.toml: when a +# newer release of a dependency needs a newer compiler, resolve an older one. +[resolver] +incompatible-rust-versions = "fallback" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 685b3d89..6b601385 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: - 'build.rs' - 'Cargo.toml' - 'Cargo.lock' + - '.cargo/**' - '.github/workflows/ci.yml' pull_request: branches: [main] @@ -20,6 +21,7 @@ on: - 'build.rs' - 'Cargo.toml' - 'Cargo.lock' + - '.cargo/**' - '.github/workflows/ci.yml' env: @@ -68,6 +70,21 @@ jobs: - name: Check compilation run: cargo check --release + # Runs on: PR to main. Enforces the rust-version declared in Cargo.toml; + # keep the toolchain below in step with it. + msrv: + name: MSRV Check + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@1.88 + - uses: Swatinem/rust-cache@v2 + - name: Create placeholder contributors file + run: touch contributors.csv + - name: Check compilation on MSRV + run: cargo check --locked --all-targets + # Runs on: push to main — fetch contributors for build-time injection prepare: name: Prepare diff --git a/AGENTS.md b/AGENTS.md index 6499bd27..de022e07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,8 +81,10 @@ Do not run the live tests unless you have been given credentials for them. ## Coding conventions -- Rust 2021 edition, `rust-version = "1.75"` in `Cargo.toml`. Do not use - features newer than the MSRV without bumping it deliberately. +- Rust 2021 edition, `rust-version = "1.88"` in `Cargo.toml`, enforced by the + `MSRV Check` job in CI. Do not use features newer than the MSRV without + bumping it deliberately; `.cargo/config.toml` makes `cargo update` prefer + dependency versions that build on it. - Keep `main.rs` thin; put logic in the library modules so it is testable in process. - Application errors are variants of `AppError` in `src/error.rs`; add a diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c796144..2598a324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Fixes + +- Upgraded the `zip` crate used to extract the downloaded StackQL binary from 0.6.6 to 9.1.3, resolving RUSTSEC-2026-0367, RUSTSEC-2026-0368 and RUSTSEC-2026-0369 and the critical "zip cloaking" dual-header parser differential advisory fixed in 9.0.5 ([#63](https://github.com/stackql/stackql-deploy-rs/issues/63)). Only the pure-Rust deflate backend is now compiled in, which also drops the `time` 0.3.41 (RUSTSEC-2026-0009), `pbkdf2` and `base64ct` dependencies. Archive extraction now fails on any entry that is a symlink or whose path would leave the destination directory; previously such path entries were silently skipped. Regular files are still written without applying permission bits from the archive. +- Updated `bytes` to 1.12.1 (RUSTSEC-2026-0007) and `crossbeam-epoch` to 0.9.21 (RUSTSEC-2026-0204), so `cargo audit` reports no vulnerabilities. +- Raised the declared minimum supported Rust version from 1.75, which the crate had not built on for several releases, to 1.88, as required by `zip` 9. A new `MSRV Check` CI job runs `cargo check --locked` on that toolchain, and `.cargo/config.toml` makes `cargo update` prefer dependency versions compatible with it. +- Website: resolved critical advisories in transitive Docusaurus build dependencies by updating `proxy-addr` to 2.0.8, `websocket-driver` to 0.7.5, `shell-quote` to 1.12.0 and, through a Yarn resolution, `tinypool` to 2.2.0. + ## 2.4.0 (2026-10-21) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 267b676f..dd10ab79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -141,12 +130,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - [[package]] name = "bitflags" version = "1.3.2" @@ -184,37 +167,11 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" @@ -222,8 +179,6 @@ version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -261,7 +216,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -286,16 +241,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.5.36" @@ -360,7 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -376,12 +321,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -408,9 +347,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] @@ -427,9 +366,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] @@ -450,15 +389,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - [[package]] name = "deunicode" version = "1.6.1" @@ -473,7 +403,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -589,6 +518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -748,15 +678,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "1.5.0" @@ -1072,15 +993,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1110,16 +1022,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.2", - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1138,9 +1040,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libm" @@ -1158,6 +1060,15 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1236,10 +1147,16 @@ dependencies = [ ] [[package]] -name = "num-conv" -version = "0.1.0" +name = "nix" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", +] [[package]] name = "num-traits" @@ -1280,29 +1197,6 @@ dependencies = [ "regex", ] -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1404,24 +1298,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1770,7 +1652,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.25.1", "radix_trie", "scopeguard", "unicode-segmentation", @@ -1857,17 +1739,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - [[package]] name = "sha2" version = "0.10.8" @@ -2092,25 +1963,6 @@ dependencies = [ "syn", ] -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - [[package]] name = "tinystr" version = "0.7.6" @@ -2222,6 +2074,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typenum" version = "1.18.0" @@ -2522,7 +2380,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2539,7 +2397,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings", ] @@ -2572,13 +2430,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2587,7 +2451,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2617,6 +2481,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2854,49 +2727,22 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.6" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb" dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq", "crc32fast", - "crossbeam-utils", "flate2", - "hmac", - "pbkdf2", - "sha1", - "time", - "zstd", -] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", + "getrandom 0.4.3", + "indexmap", + "memchr", + "nix 0.31.3", + "typed-path", + "windows-sys 0.61.2", ] [[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +name = "zlib-rs" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" diff --git a/Cargo.toml b/Cargo.toml index fb2f5d22..712b84a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "stackql-deploy" version = "2.4.0" edition = "2021" -rust-version = "1.75" +rust-version = "1.88" description = "Infrastructure-as-code framework for declarative cloud resource management using StackQL" authors = ["StackQL Studios "] license = "MIT" @@ -36,7 +36,7 @@ rustyline = "10.0" tera = "1.19.0" log = "0.4" env_logger = "0.10" -zip = "0.6" +zip = { version = "9.1", default-features = false, features = ["deflate-flate2-zlib-rs"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } indicatif = "0.17" unicode-width = "0.1.10" diff --git a/src/utils/download.rs b/src/utils/download.rs index 4f66287f..d9181f2e 100644 --- a/src/utils/download.rs +++ b/src/utils/download.rs @@ -198,28 +198,7 @@ fn extract_binary( } _ => { // For Windows and Linux, we use the zip file - let file = File::open(archive_path).map_err(AppError::IoError)?; - let mut archive = ZipArchive::new(file).map_err(|e| { - AppError::CommandFailed(format!("Failed to open zip archive: {}", e)) - })?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i).map_err(|e| { - AppError::CommandFailed(format!("Failed to extract file: {}", e)) - })?; - - let outpath = match file.enclosed_name() { - Some(path) => dest_dir.join(path), - _none => continue, - }; - - if file.name().ends_with('/') { - fs::create_dir_all(&outpath).map_err(AppError::IoError)?; - } else { - let mut outfile = File::create(&outpath).map_err(AppError::IoError)?; - io::copy(&mut file, &mut outfile).map_err(AppError::IoError)?; - } - } + extract_zip(archive_path, dest_dir)?; // Check if we need to rename the binary on Windows if get_platform() == Platform::Windows { @@ -241,6 +220,52 @@ fn extract_binary( Ok(binary_path) } +/// Extracts a zip archive into `dest_dir`. +/// +/// Entries are written as plain files and directories only: no symlinks are +/// created and no permission bits from the archive are applied. An entry whose +/// path would escape `dest_dir` (absolute, or containing `..`) or that is a +/// symlink fails the whole extraction rather than being skipped, since the +/// stackql release archive never contains either. +fn extract_zip(archive_path: &Path, dest_dir: &Path) -> Result<(), AppError> { + let file = File::open(archive_path).map_err(AppError::IoError)?; + let mut archive = ZipArchive::new(file) + .map_err(|e| AppError::CommandFailed(format!("Failed to open zip archive: {}", e)))?; + + for i in 0..archive.len() { + let mut entry = archive + .by_index(i) + .map_err(|e| AppError::CommandFailed(format!("Failed to extract file: {}", e)))?; + + let entry_name = String::from_utf8_lossy(entry.name_raw()).into_owned(); + let relative = entry.enclosed_name().ok_or_else(|| { + AppError::CommandFailed(format!( + "Refusing to extract zip entry with unsafe path: {}", + entry_name + )) + })?; + if entry.is_symlink() { + return Err(AppError::CommandFailed(format!( + "Refusing to extract symlink zip entry: {}", + entry_name + ))); + } + let outpath = dest_dir.join(relative); + + if entry.is_dir() { + fs::create_dir_all(&outpath).map_err(AppError::IoError)?; + } else { + if let Some(parent) = outpath.parent() { + fs::create_dir_all(parent).map_err(AppError::IoError)?; + } + let mut outfile = File::create(&outpath).map_err(AppError::IoError)?; + io::copy(&mut entry, &mut outfile).map_err(AppError::IoError)?; + } + } + + Ok(()) +} + /// Recursively search for a file by name in a directory tree. /// Returns the first match that is a regular file (not a directory). fn find_file_recursive(dir: &Path, target_name: &str) -> Option { @@ -257,3 +282,82 @@ fn find_file_recursive(dir: &Path, target_name: &str) -> Option { } None } + +#[cfg(test)] +mod tests { + use super::*; + use zip::write::SimpleFileOptions; + use zip::{CompressionMethod, ZipWriter}; + + enum Entry<'a> { + File(&'a str, &'a [u8]), + Dir(&'a str), + Symlink(&'a str, &'a str), + } + + fn write_zip(path: &Path, entries: &[Entry]) { + let mut zip = ZipWriter::new(File::create(path).unwrap()); + let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + for entry in entries { + match entry { + Entry::File(name, data) => { + zip.start_file(*name, options).unwrap(); + zip.write_all(data).unwrap(); + } + Entry::Dir(name) => zip.add_directory(*name, options).unwrap(), + Entry::Symlink(name, target) => zip.add_symlink(*name, *target, options).unwrap(), + } + } + zip.finish().unwrap(); + } + + #[test] + fn extract_zip_writes_files_and_directories() { + let tmp = tempfile::tempdir().unwrap(); + let archive = tmp.path().join("ok.zip"); + let dest = tmp.path().join("out"); + fs::create_dir(&dest).unwrap(); + write_zip( + &archive, + &[ + Entry::File("stackql", b"binary"), + Entry::Dir("docs/"), + Entry::File("docs/readme.txt", b"hello"), + ], + ); + + extract_zip(&archive, &dest).unwrap(); + + assert_eq!(fs::read(dest.join("stackql")).unwrap(), b"binary"); + assert!(dest.join("docs").is_dir()); + assert_eq!(fs::read(dest.join("docs/readme.txt")).unwrap(), b"hello"); + } + + #[test] + fn extract_zip_rejects_path_traversal() { + let tmp = tempfile::tempdir().unwrap(); + let archive = tmp.path().join("traversal.zip"); + let dest = tmp.path().join("out"); + fs::create_dir(&dest).unwrap(); + write_zip(&archive, &[Entry::File("../escaped.txt", b"pwned")]); + + let err = extract_zip(&archive, &dest).unwrap_err(); + + assert!(err.to_string().contains("unsafe path"), "{}", err); + assert!(!tmp.path().join("escaped.txt").exists()); + } + + #[test] + fn extract_zip_rejects_symlinks() { + let tmp = tempfile::tempdir().unwrap(); + let archive = tmp.path().join("symlink.zip"); + let dest = tmp.path().join("out"); + fs::create_dir(&dest).unwrap(); + write_zip(&archive, &[Entry::Symlink("stackql", "/etc/passwd")]); + + let err = extract_zip(&archive, &dest).unwrap_err(); + + assert!(err.to_string().contains("symlink"), "{}", err); + assert!(fs::symlink_metadata(dest.join("stackql")).is_err()); + } +} diff --git a/website/package.json b/website/package.json index c3b1fb53..e3dfe930 100644 --- a/website/package.json +++ b/website/package.json @@ -57,5 +57,8 @@ }, "engines": { "node": ">=20.0" + }, + "resolutions": { + "tinypool": "^2.1.2" } } diff --git a/website/yarn.lock b/website/yarn.lock index ee3459cb..0850e574 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -10552,9 +10552,9 @@ proto-list@~1.2.1: integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932" + integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ== dependencies: forwarded "0.2.0" ipaddr.js "1.9.1" @@ -11364,9 +11364,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== + version "1.12.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.12.0.tgz#e98fdfd2e921d8a58ae2820f4fc0f75c39f09675" + integrity sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q== side-channel-list@^1.0.0: version "1.0.0" @@ -11884,10 +11884,10 @@ tinyexec@^1.0.1: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== -tinypool@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" - integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== +tinypool@^1.0.2, tinypool@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.2.0.tgz#cbd3f850b02f132ad288546bbc14c42cd93b32e5" + integrity sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ== to-fast-properties@^2.0.0: version "2.0.0" @@ -12493,9 +12493,9 @@ webpackbar@^6.0.1: wrap-ansi "^7.0.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + version "0.7.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.5.tgz#569d22764ab21f2de20af0e74b411e8ae5a0fa46" + integrity sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA== dependencies: http-parser-js ">=0.5.1" safe-buffer ">=5.1.0"