Repository navigation
fix: apply central directory metadata in ZipStreamReader, only to written entries - #1030
Pr0methean merged 11 commits into
Conversation
…tten entries ZipStreamReader::visit never called visit_additional_metadata. read_zipfile_from_stream returns None after reading the signature of the first central directory header, and parse_central_directory then parsed the header from scratch, expecting the signature again; it failed with "Invalid Central Directory header", which the `while let Ok(..)` loop swallowed. So ZipStreamReader::extract never applied unix permissions: a 0o751 file extracted with the process default mode. Put the consumed signature back in front of the stream so every central directory header parses. That makes the metadata pass of `extract` run for the first time, and it set permissions on `destination.join(central directory name)`. Nothing checks central directory names against the local headers, and set_permissions follows symlinks: with a destination that already contains `a` -> elsewhere, a central directory entry `a/x` (local entry named differently) would chmod elsewhere/x. Record the path visit_file checked and wrote for each local entry, and apply a central directory mode only to that path; names this pass did not write get no mode. Apply modes children first, as ZipArchive::extract does, and never through a symlink. Tests (tests/stream.rs): visit sees every central directory entry (master: 0 of 3); extract applies 751/600/644 (master: process default); a mode attached to a central-directory-only name is not applied through a pre-existing symlink (fails with the first, unhardened version of this fix).
Pr0methean
left a comment
There was a problem hiding this comment.
Good start but still needs some work.
Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
|
@yuxi-liu-wired This is failing tests and will probably do so even once #1035 is fixed and merged. |
|
@Pr0methean I checked this locally. Both red checks on b90d6c3 come from master, not from this PR (which only touches
With #1035 (7b2fa0b) merged into this branch, the one-line conflict in |
Signed-off-by: n4n5 <its.just.n4n5@gmail.com>
They create a temp dir, which miri's isolation does not allow; the endianness job runs the tests under miri since zip-rs#1035, which marked its own file-system tests the same way.
|
The endianness job failed after the master merge: since #1035 it runs the tests under miri, and this PR's three file-system tests in |
|
@yuxi-liu-wired Could you please share the fuzzers you used to find this bug and the ones in #1024, #1025, #1027 and #1028? I'm wondering if we might be able to use some of their code to increase the coverage of our existing fuzzers. |
|
@Pr0methean sure, here they are: https://gist.github.com/yuxi-liu-wired/ce659d5876d86056ff82621b9af1d8a1 They're five cargo-fuzz targets that check zip2's documented promises as end-state invariants, not just "no panic". Each file's header lists its checks. The README maps each of these PRs to the target that found it: #1024 came from |
Problem
ZipStreamReader::visitnever callsZipStreamVisitor::visit_additional_metadata, soZipStreamReader::extractnever applies the unix permissions from the central directory, thoughits documentation says the permissions are applied after the files are created. Every file and
directory gets the process default instead: a
0o600file comes out0o644(umask 0022),readable by every local user, and a
0o700directory comes out listable.ZipArchive::extracton the same archive restores the modes.The cause:
read_zipfile_from_streamreads 4 bytes, sees the central directory header signature andreturns
Ok(None). The stream is now 4 bytes into the first central directory header.visitthen callsparse_central_directory, which reads the signature again: it gets thenext 4 bytes of the header body and fails with "Invalid Central Directory header".
while let Ok(metadata) = self.parse_central_directory()swallows the error, sovisitreturns
Ok(())without callingvisit_additional_metadata.The in-crate test
zip_read_streaming_visitoronly asserts insidevisit_additional_metadata, so it passes when that method is never called. Every release withread::streamis affected (checked on 0.6.5, 8.6.0 and master).Why the extraction side changes too
Fixing only the offset would run
Extractor::visit_additional_metadatafor the first time, and aswritten it is not safe: it applies the mode to
destination.join(central directory name)withfs::set_permissions, which follows symlinks. zip2 does not check central directory names againstthe local headers, so a central directory entry can name a path that no local entry wrote. With a
symlink already in the destination (
dest/a -> ../outside), an entry whose local name isc_d.txtand whose central name isa/b.txtwith mode0o777would chmodoutside/b.txt,while
visit_filecorrectly refuses to write through that link.Change
visitputs the consumed signature back in front of the stream before parsing the centraldirectory, so every header parses.
extractrecords, for each local entry, the pathvisit_filechecked (safe_prepare_path)and wrote. A central directory mode applies only to that path; a name this pass did not write
gets no mode.
ZipArchive::extractdoes), and neverthrough a symlink (
symlink_metadatacheck beforeset_permissions).Like
ZipArchive::extract, this does not try to protect against another process changing thedestination during the extraction.
Tests
In
tests/stream.rs:visit_sees_every_central_directory_entry: 3 local and 3 central entries (master: 0 central).extract_applies_unix_modes: 751 / 600 / 644 (master: the process default).extract_ignores_modes_for_names_it_did_not_write: the reproduction above; fails if a nameonly the central directory carries gets a mode.
extract_ignores_modes_for_a_name_that_is_a_symlink_it_did_not_write: the central-only nameis itself a pre-existing symlink to a file outside; fails if, in addition, the symlink check is
removed.
The suite passes on 1.88, stable and nightly with default features,
--no-default-features,--all-features, and the two single-deflate feature sets;cargo clippy --all-targets -- -D warnings(default,--no-default-features,--all-features) andcargo doc --no-depsareclean;
cargo fmt --check --allis clean.Found with a fuzzer that drives both extraction paths:
read/stream.rsstayed at 63% linecoverage because the central directory pass never ran.