diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d992603..60f6170 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,10 @@ jobs: uses: taiki-e/install-action@v2 with: tool: just,nextest,cargo-llvm-cov + - name: Run cargo doc, deny warnings + run: | + export RUSTDOCFLAGS="-D warnings" + cargo doc --all-features --no-deps - name: Run cargo clippy run: | cargo clippy --all-targets --all-features diff --git a/crates/rc-zip/src/format/archive.rs b/crates/rc-zip/src/format/archive.rs index 5c9240c..d3b7618 100644 --- a/crates/rc-zip/src/format/archive.rs +++ b/crates/rc-zip/src/format/archive.rs @@ -4,7 +4,7 @@ use crate::format::*; /// along with a list of [entries][StoredEntry]. /// /// It is obtained via an [ArchiveReader](crate::reader::ArchiveReader), or via a higher-level API -/// like the [ReadZip](crate::reader::ReadZip) trait. +/// like the [ReadZip](crate::reader::sync::ReadZip) trait. pub struct Archive { pub(crate) size: u64, pub(crate) encoding: Encoding, @@ -161,14 +161,53 @@ pub struct StoredEntryInner { } impl StoredEntry { - /// Returns the entry's name + /// Returns the entry's name. See also + /// [sanitized_name()](StoredEntry::sanitized_name), which returns a + /// sanitized version of the name. /// - /// This should be a relative path, separated by `/`. However, there are zip files in the wild - /// with all sorts of evil variants, so, be conservative in what you accept. + /// This should be a relative path, separated by `/`. However, there are zip + /// files in the wild with all sorts of evil variants, so, be conservative + /// in what you accept. pub fn name(&self) -> &str { self.entry.name.as_ref() } + /// Returns a sanitized version of the entry's name, if it + /// seems safe. In particular, if this method feels like the + /// entry name is trying to do a zip slip (cf. + /// ), it'll return + /// None. + /// + /// Other than that, it will strip any leading slashes on non-Windows OSes. + pub fn sanitized_name(&self) -> Option<&str> { + let name = self.name(); + + // refuse entries with traversed/absolute path to mitigate zip slip + if name.contains("..") { + return None; + } + + #[cfg(windows)] + { + if name.contains(":\\") || name.starts_with("\\") { + return None; + } + Some(name) + } + + #[cfg(not(windows))] + { + // strip absolute prefix on entries pointing to root path + let mut entry_chars = name.chars(); + let mut name = name; + while name.starts_with('/') { + entry_chars.next(); + name = entry_chars.as_str() + } + Some(name) + } + } + /// The entry's comment, if any. /// /// When reading a zip file, an empty comment results in None. diff --git a/crates/rc-zip/src/lib.rs b/crates/rc-zip/src/lib.rs index d0cdcf2..7ca49bf 100644 --- a/crates/rc-zip/src/lib.rs +++ b/crates/rc-zip/src/lib.rs @@ -4,12 +4,12 @@ //! //! ### Reading //! -//! [ArchiveReader](ArchiveReader) is your first stop. It +//! [ArchiveReader](reader::ArchiveReader) is your first stop. It //! ensures we are dealing with a valid zip archive, and reads the central //! directory. It does not perform I/O itself, but rather, it is a state machine //! that asks for reads at specific offsets. //! -//! An [Archive](Archive) contains a full list of [entries](types::StoredEntry), +//! An [Archive] contains a full list of [entries](StoredEntry), //! which you can then extract. //! //! ### Writing