Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src-tauri/src/core/services/downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tauri::{AppHandle, Emitter};
use tokio::io::AsyncWriteExt;
use tokio::sync::Semaphore;

use crate::core::services::mod_store::read_entry_bounded;
use crate::core::services::portal_client::{encode_path_component, plausible_name};
use crate::error::AppError;
use crate::models::{InstalledChangedPayload, InstalledChangedReason};
Expand Down Expand Up @@ -458,8 +459,7 @@ fn verify_mod_zip_sync(path: &Path, expected_name: &str, expected_version: &str)
|| entry_name.ends_with("\\info.json");
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)
let s = read_entry_bounded(&mut entry)
.map_err(|e| AppError::Parse(format!("could not read info.json: {e}")))?;
let is_root = name == "info.json";
info_json = Some((name, s));
Expand Down
33 changes: 29 additions & 4 deletions src-tauri/src/core/services/mod_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ use crate::models::{

const MOD_LIST_FILE: &str = "mod-list.json";

/// Maximum allowable decompressed size for an info.json entry (2 MiB).
/// Protects against zip bomb / memory exhaustion DoS attacks during archive reading.
pub const MAX_INFO_JSON_SIZE: u64 = 2 * 1024 * 1024;

/// Read up to `MAX_INFO_JSON_SIZE` bytes from a zip entry into a String.
pub fn read_entry_bounded<R: Read>(mut entry: R) -> Result<String, AppError> {
let mut s = String::new();
entry.by_ref().take(MAX_INFO_JSON_SIZE + 1).read_to_string(&mut s)?;
if s.len() > MAX_INFO_JSON_SIZE as usize {
return Err(AppError::Parse(format!(
"info.json exceeds maximum size limit of {MAX_INFO_JSON_SIZE} bytes"
)));
}
Ok(s)
}

// ---------------------------------------------------------------------------
// Directory auto-detection (Phase 3)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -238,15 +254,13 @@ pub fn read_info_json(path: &Path) -> Result<InfoJson, AppError> {
}
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)?;
let s = read_entry_bounded(&mut entry)?;
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)?;
let s = read_entry_bounded(&mut entry)?;
nested = Some(s);
}
}
Expand Down Expand Up @@ -1755,4 +1769,15 @@ mod tests {

let _ = fs::remove_dir_all(&temp_dir);
}

#[test]
fn test_oversized_info_json_rejected() {
let huge_data = vec![b'a'; (MAX_INFO_JSON_SIZE + 100) as usize];
let cursor = std::io::Cursor::new(huge_data);
let err = read_entry_bounded(cursor).unwrap_err();
assert!(
err.to_string().contains("exceeds maximum size limit"),
"expected size limit error, got: {err}"
);
}
}
Loading