From c520899b35ea834c608f0c59dcb854b596a4ba71 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:25:10 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Fix=20zip=20bomb=20?= =?UTF-8?q?vulnerability=20by=20bounding=20info.json=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforce a 10 MB maximum decompressed size limit on info.json entry reads across mod_store.rs and downloader.rs to mitigate zip bomb / DoS risks. Co-authored-by: TheDragonSoft <75906592+TheDragonSoft@users.noreply.github.com> --- src-tauri/src/core/services/downloader.rs | 5 +++- src-tauri/src/core/services/mod_store.rs | 35 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/core/services/downloader.rs b/src-tauri/src/core/services/downloader.rs index fc8b739..a06bcd2 100644 --- a/src-tauri/src/core/services/downloader.rs +++ b/src-tauri/src/core/services/downloader.rs @@ -21,6 +21,8 @@ const MAX_CONCURRENT_DOWNLOADS: usize = 3; const PROGRESS_INTERVAL: Duration = Duration::from_millis(120); /// Automatic retries for transient failures (network blips, 429, 5xx). const MAX_ATTEMPTS: u32 = 3; +/// Maximum allowed decompressed size for `info.json` (10 MB) to protect against zip bombs / DoS. +const MAX_INFO_JSON_SIZE: u64 = 10 * 1024 * 1024; /// Event payload for "download-updated" — mirrors the frontend QueueItem. #[derive(Debug, Clone, Serialize)] @@ -459,7 +461,8 @@ fn verify_mod_zip_sync(path: &Path, expected_name: &str, expected_version: &str) if !entry.is_dir() && is_info_json { let name = entry_name.to_string(); let mut s = String::new(); - std::io::Read::read_to_string(&mut entry, &mut s) + // SECURITY: Bound decompressed size with take() to prevent zip bombs/unbounded memory allocation. + std::io::Read::read_to_string(&mut entry.take(MAX_INFO_JSON_SIZE), &mut s) .map_err(|e| AppError::Parse(format!("could not read info.json: {e}")))?; let is_root = name == "info.json"; info_json = Some((name, s)); diff --git a/src-tauri/src/core/services/mod_store.rs b/src-tauri/src/core/services/mod_store.rs index 340fee3..9489ac9 100644 --- a/src-tauri/src/core/services/mod_store.rs +++ b/src-tauri/src/core/services/mod_store.rs @@ -18,6 +18,9 @@ use crate::models::{ const MOD_LIST_FILE: &str = "mod-list.json"; +/// Maximum allowed decompressed size for `info.json` (10 MB) to protect against zip bombs / DoS. +const MAX_INFO_JSON_SIZE: u64 = 10 * 1024 * 1024; + // --------------------------------------------------------------------------- // Directory auto-detection (Phase 3) // --------------------------------------------------------------------------- @@ -239,14 +242,16 @@ pub fn read_info_json(path: &Path) -> Result { let entry_name = entry.name().to_string(); if entry_name == "info.json" || entry_name == "./info.json" { let mut s = String::new(); - entry.read_to_string(&mut s)?; + // SECURITY: Bound decompressed size with take() to prevent zip bombs/unbounded memory allocation. + entry.take(MAX_INFO_JSON_SIZE).read_to_string(&mut s)?; raw = Some(s); break; } else if nested.is_none() && (entry_name.ends_with("/info.json") || entry_name.ends_with("\\info.json")) { let mut s = String::new(); - entry.read_to_string(&mut s)?; + // SECURITY: Bound decompressed size with take() to prevent zip bombs/unbounded memory allocation. + entry.take(MAX_INFO_JSON_SIZE).read_to_string(&mut s)?; nested = Some(s); } } @@ -1755,4 +1760,30 @@ mod tests { let _ = fs::remove_dir_all(&temp_dir); } + + #[test] + fn test_oversized_info_json_is_truncated_safely() { + let temp_dir = std::env::temp_dir().join(format!("axial-zipbomb-test-{}", std::process::id())); + let _ = fs::create_dir_all(&temp_dir); + + let path = temp_dir.join("oversized_1.0.0.zip"); + let file = fs::File::create(&path).unwrap(); + let mut zip = zip::ZipWriter::new(file); + zip.start_file("info.json", zip::write::SimpleFileOptions::default()).unwrap(); + + // Write a valid json prefix followed by huge padding that exceeds MAX_INFO_JSON_SIZE + let prefix = b"{\"name\":\"oversized\",\"version\":\"1.0.0\","; + zip.write_all(prefix).unwrap(); + // Write padding bytes (e.g. 11 MB of spaces) + let padding = vec![b' '; (MAX_INFO_JSON_SIZE + 1024) as usize]; + zip.write_all(&padding).unwrap(); + zip.write_all(b"}").unwrap(); + zip.finish().unwrap(); + + // Attempting to read_info_json should truncate at MAX_INFO_JSON_SIZE and fail to parse as JSON cleanly + // rather than consuming infinite memory. + assert!(read_info_json(&path).is_err()); + + let _ = fs::remove_dir_all(&temp_dir); + } } From e91a78f0e8575e3bb9dad5aa67bc771a3fac59c9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:31:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Fix=20zip=20bomb=20?= =?UTF-8?q?vulnerability=20by=20bounding=20info.json=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforce a 10 MB maximum decompressed size limit on info.json entry reads across mod_store.rs and downloader.rs to mitigate zip bomb / DoS risks. Added trait imports and verified unit test suite. Co-authored-by: TheDragonSoft <75906592+TheDragonSoft@users.noreply.github.com> --- src-tauri/src/core/services/downloader.rs | 3 ++- src-tauri/src/core/services/mod_store.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/core/services/downloader.rs b/src-tauri/src/core/services/downloader.rs index a06bcd2..da04443 100644 --- a/src-tauri/src/core/services/downloader.rs +++ b/src-tauri/src/core/services/downloader.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::io::Read; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; @@ -462,7 +463,7 @@ fn verify_mod_zip_sync(path: &Path, expected_name: &str, expected_version: &str) let name = entry_name.to_string(); let mut s = String::new(); // SECURITY: Bound decompressed size with take() to prevent zip bombs/unbounded memory allocation. - std::io::Read::read_to_string(&mut entry.take(MAX_INFO_JSON_SIZE), &mut s) + entry.take(MAX_INFO_JSON_SIZE).read_to_string(&mut s) .map_err(|e| AppError::Parse(format!("could not read info.json: {e}")))?; let is_root = name == "info.json"; info_json = Some((name, s)); diff --git a/src-tauri/src/core/services/mod_store.rs b/src-tauri/src/core/services/mod_store.rs index 9489ac9..224d405 100644 --- a/src-tauri/src/core/services/mod_store.rs +++ b/src-tauri/src/core/services/mod_store.rs @@ -233,7 +233,7 @@ pub fn read_info_json(path: &Path) -> Result { let mut raw: Option = None; let mut nested: Option = None; for i in 0..archive.len() { - let mut entry = archive + let entry = archive .by_index(i) .map_err(|e| AppError::Parse(format!("zip read error: {e}")))?; if entry.is_dir() {