mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
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:
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user