Write CRC-mismatched ZIP members when if_tampered=continue (#525)

Fixes #518. With if_tampered=continue, core_archive_extract writes a ZIP member whose CRC does not match (the mismatch stays in contradictions), as its remediation says.
This commit is contained in:
NetterSilver
2026-11-03 08:59:03 +01:00
committed by GitHub
parent a007d128af
commit d3bbf99b4a
2 changed files with 38 additions and 5 deletions
+28 -1
View File
@@ -690,7 +690,15 @@ fn core_archive_extract(ctx: &mut Ctx<'_>, params: Value) -> Result<ToolOutput,
Ok(d) => members.push((e.name.clone(), Member::Owned(d))),
Err(err @ zip::ZipError::CrcMismatch { .. }) => {
contradictions.push(json!({"path": e.name, "reason": err.to_string()}));
members.push((e.name.clone(), Member::Skip(err.to_string())));
let m = if p.if_tampered == IntegrityPolicy::Continue {
match zip::read_entry_unverified(&bytes, e, p.max_bytes) {
Ok(d) => Member::Owned(d),
Err(read_err) => Member::Skip(read_err.to_string()),
}
} else {
Member::Skip(err.to_string())
};
members.push((e.name.clone(), m));
}
Err(err) => members.push((e.name.clone(), Member::Skip(err.to_string()))),
}
@@ -981,6 +989,25 @@ mod tests {
assert_eq!(ok["result"]["contradictions"].as_array().unwrap().len(), 1);
}
#[test]
fn extract_if_tampered_continue_writes_the_member() {
let dir = tempfile::tempdir().unwrap();
let mut z = fx::zip_stored(&[("a.txt", b"hello")]);
let pos = z.windows(5).position(|w| w == b"hello").unwrap();
z[pos] = b'j';
let p = dir.path().join("bad.zip");
std::fs::write(&p, &z).unwrap();
let e = Engine::new(crate::testing::session());
let err = call(&e, "core_archive_extract", json!({"path": p})).unwrap_err();
assert_eq!(err.code, ErrorCode::FileHashMismatch);
let ok = call(&e, "core_archive_extract", json!({"path": p, "if_tampered": "continue"})).unwrap();
let r = &ok["result"];
assert_eq!(r["files_written"], 1, "{r}");
assert_eq!(r["contradictions"].as_array().unwrap().len(), 1);
let out = std::path::PathBuf::from(r["output_directory"].as_str().unwrap());
assert_eq!(std::fs::read(out.join("a.txt")).unwrap(), b"jello");
}
#[test]
fn safe_components_rules() {
assert!(safe_components("../x").is_err());
+10 -4
View File
@@ -231,6 +231,16 @@ pub fn raw_entry_data<'a>(data: &'a [u8], e: &ZipEntry) -> Result<&'a [u8], ZipE
/// Read and decompress one entry, refusing to produce more than `max_bytes`.
pub fn read_entry(data: &[u8], e: &ZipEntry, max_bytes: u64) -> Result<Vec<u8>, ZipError> {
let out = read_entry_unverified(data, e, max_bytes)?;
let found = crc32(&out);
if found != e.crc32 {
return Err(ZipError::CrcMismatch { expected: e.crc32, found });
}
Ok(out)
}
/// Like [`read_entry`] but without comparing the stored CRC-32, so a caller that accepts a mismatch can still get the bytes.
pub fn read_entry_unverified(data: &[u8], e: &ZipEntry, max_bytes: u64) -> Result<Vec<u8>, ZipError> {
if e.encrypted {
return Err(ZipError::Unsupported(format!("{} is encrypted", e.name)));
}
@@ -248,10 +258,6 @@ pub fn read_entry(data: &[u8], e: &ZipEntry, max_bytes: u64) -> Result<Vec<u8>,
93 => crate::decompress::zstd(raw, max_bytes).map_err(|err| ZipError::Corrupt(format!("zstd stream for {} is damaged: {err}", e.name)))?,
m => return Err(ZipError::Unsupported(format!("compression method {m} ({}) for {}", e.method_name(), e.name))),
};
let found = crc32(&out);
if found != e.crc32 {
return Err(ZipError::CrcMismatch { expected: e.crc32, found });
}
Ok(out)
}