fix: bound ugoira archive expansion

This commit is contained in:
2026-09-24 18:44:21 +08:00
parent 4694e60ee3
commit 1f9da14772
+30 -31
View File
@@ -210,9 +210,17 @@ impl PixivAPI {
return Ok(None); return Ok(None);
} }
let metadata = self.ugoira_metadata(illust_id).await?; let metadata = self.ugoira_metadata(illust_id).await?;
const MAX_UGOIRA_FRAMES: usize = 5_000;
const MAX_UGOIRA_UNPACKED_BYTES: u64 = 512 * 1024 * 1024;
if metadata.frames.is_empty() { if metadata.frames.is_empty() {
return Ok(None); return Ok(None);
} }
if metadata.frames.len() > MAX_UGOIRA_FRAMES {
return Err(PixivError::Api(format!(
"ugoira has too many frames: {}",
metadata.frames.len()
)));
}
let zip_url = metadata let zip_url = metadata
.zip_url .zip_url
.clone() .clone()
@@ -267,10 +275,6 @@ impl PixivAPI {
.prefix(crate::TEMP_FILE_PREFIX) .prefix(crate::TEMP_FILE_PREFIX)
.tempdir() .tempdir()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
// Extract frames to canonical zero-padded names; pixiv ugoira
// frames are uniformly jpg or png per artwork. The zip is read
// from disk; `zip_file` stays alive for the whole extraction.
let mut archive = zip::ZipArchive::new( let mut archive = zip::ZipArchive::new(
std::fs::File::open(zip_file.path()).map_err(|e| e.to_string())?, std::fs::File::open(zip_file.path()).map_err(|e| e.to_string())?,
) )
@@ -278,34 +282,15 @@ impl PixivAPI {
if archive.is_empty() { if archive.is_empty() {
return Err("empty frame zip".to_string()); return Err("empty frame zip".to_string());
} }
// Uniform jpg or png per artwork; sniff the first entry's if archive.len() > MAX_UGOIRA_FRAMES {
// magic bytes instead of trusting its filename. return Err("ugoira frame zip has too many entries".to_string());
let first = archive.by_index(0).map_err(|e| e.to_string())?;
let mut first_bytes = Vec::new();
first
.take(64 * 1024 * 1024 + 1)
.read_to_end(&mut first_bytes)
.map_err(|e| e.to_string())?;
if first_bytes.len() > 64 * 1024 * 1024 {
return Err("frame exceeds size cap".to_string());
} }
let extension = if first_bytes.starts_with(&[0xFF, 0xD8]) {
"jpg"
} else if first_bytes.starts_with(b"\x89PNG") {
"png"
} else {
"jpg"
};
let mut count = 0usize; let mut count = 0usize;
{ let mut unpacked = 0u64;
let path = frames_dir let mut extension = "jpg";
.path() for i in 0..archive.len() {
.join(format!("img_{count:05}.{extension}")); let mut entry = archive.by_index(i).map_err(|e| e.to_string())?;
std::fs::write(&path, &first_bytes).map_err(|e| e.to_string())?;
count += 1;
}
for i in 1..archive.len() {
let entry = archive.by_index(i).map_err(|e| e.to_string())?;
if entry.size() > 64 * 1024 * 1024 { if entry.size() > 64 * 1024 * 1024 {
return Err(format!("frame {i} exceeds size cap")); return Err(format!("frame {i} exceeds size cap"));
} }
@@ -317,6 +302,21 @@ impl PixivAPI {
if bytes.len() > 64 * 1024 * 1024 { if bytes.len() > 64 * 1024 * 1024 {
return Err(format!("frame {i} exceeds size cap")); return Err(format!("frame {i} exceeds size cap"));
} }
unpacked = unpacked
.checked_add(bytes.len() as u64)
.ok_or_else(|| "ugoira unpacked size overflow".to_string())?;
if unpacked > MAX_UGOIRA_UNPACKED_BYTES {
return Err("ugoira exceeds total unpacked size cap".to_string());
}
if i == 0 {
extension = if bytes.starts_with(&[0xFF, 0xD8]) {
"jpg"
} else if bytes.starts_with(b"\x89PNG") {
"png"
} else {
"jpg"
};
}
let path = frames_dir let path = frames_dir
.path() .path()
.join(format!("img_{count:05}.{extension}")); .join(format!("img_{count:05}.{extension}"));
@@ -326,7 +326,6 @@ impl PixivAPI {
if count == 0 { if count == 0 {
return Err("empty frame zip".to_string()); return Err("empty frame zip".to_string());
} }
// Constant rate from the median frame delay (ms). // Constant rate from the median frame delay (ms).
let mut delays = frame_delays; let mut delays = frame_delays;
delays.sort_unstable(); delays.sort_unstable();