summaryrefslogtreecommitdiffstats
path: root/src/mods.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-29 02:24:41 +0200
committersalmonik <salmonik@tuta.io>2026-07-29 02:24:41 +0200
commitc654a6d17eb58adbb15b9c47ab32bcc3a1666307 (patch)
tree545544bf7d805254415da787fad1cdc711767833 /src/mods.rs
parentAdd per-instance mod management with client-side detection (diff)
downloadminecraft-ui-c654a6d17eb58adbb15b9c47ab32bcc3a1666307.tar.gz
minecraft-ui-c654a6d17eb58adbb15b9c47ab32bcc3a1666307.zip
Security audit fixes: patch 16 vulnerabilities across 7 source files
CRITICAL: - Instance name path traversal (AP-001): sanitize::sanitize_instance_name() strips .., /, \, \0, spaces, special chars at all 12 join call sites - Zip-slip protection in world import: starts_with() guard after enclosed_name() HIGH: - RCON command injection: validate_username() rejects non-MC usernames across all 6 player action macros (whitelist, op, ban) - Level-seed properties injection: sanitize_seed() strips \n \r = : \0 - CSRF protection: SameSite=Lax -> SameSite=Strict on session cookie MEDIUM: - Session TTL: server-side 24h expiry via strftime check in require_auth - Rate limiting: in-memory 10-ip/5min sliding window on login - Background task stale is_active: all queries use concrete WHERE id=?1 - Mod validation DoS: 500MB cap, 10k class scan limit, 10k CP entry cap LOW: - Logout auth guard: moved into protected route group + CurrentUser param - Mod orphan race: DB insert before file write - Backup orphan race: DB insert before ZIP creation - Template fix: broken Askama == comparisons in edit.html All fixes compile clean with zero warnings. 10 unit tests pass.
Diffstat (limited to '')
-rw-r--r--src/mods.rs31
1 files changed, 30 insertions, 1 deletions
diff --git a/src/mods.rs b/src/mods.rs
index a3f782b..10799f0 100644
--- a/src/mods.rs
+++ b/src/mods.rs
@@ -43,6 +43,9 @@ pub struct ValidationResult {
43 pub side: ModSide, 43 pub side: ModSide,
44} 44}
45 45
46/// Maximum size for a single mod JAR file (500 MB).
47const MAX_MOD_SIZE_BYTES: usize = 500 * 1024 * 1024;
48
46/// Validate a mod JAR (raw bytes, filename for error messages). 49/// Validate a mod JAR (raw bytes, filename for error messages).
47/// 50///
48/// Returns a `ValidationResult` indicating whether the mod is safe to install. 51/// Returns a `ValidationResult` indicating whether the mod is safe to install.
@@ -56,6 +59,19 @@ pub struct ValidationResult {
56/// (`net/minecraft/block/Block`, `net/minecraft/item/Item`, …) without ever 59/// (`net/minecraft/block/Block`, `net/minecraft/item/Item`, …) without ever
57/// referencing `net/minecraft/server/…` directly. 60/// referencing `net/minecraft/server/…` directly.
58pub fn validate_mod(data: &[u8], filename: &str) -> ValidationResult { 61pub fn validate_mod(data: &[u8], filename: &str) -> ValidationResult {
62 // Reject oversized files early to prevent DoS
63 if data.len() > MAX_MOD_SIZE_BYTES {
64 return ValidationResult {
65 accepted: false,
66 rejection_reason: Some(format!(
67 "File too large: {} bytes (maximum is {} MB)",
68 data.len(),
69 MAX_MOD_SIZE_BYTES / (1024 * 1024)
70 )),
71 display_name: None,
72 side: ModSide::Unknown,
73 };
74 }
59 // Phase 1: metadata check 75 // Phase 1: metadata check
60 let (meta_side, meta_display_name) = read_metadata(data); 76 let (meta_side, meta_display_name) = read_metadata(data);
61 77
@@ -201,6 +217,9 @@ enum ScanResult {
201 NoClearSide, 217 NoClearSide,
202} 218}
203 219
220/// Maximum number of class files to scan inside a single JAR (prevents DoS).
221const MAX_CLASSES_TO_SCAN: usize = 10_000;
222
204/// Walk all `.class` entries in the JAR and look for client vs server references. 223/// Walk all `.class` entries in the JAR and look for client vs server references.
205/// 224///
206/// Two independent signal layers: 225/// Two independent signal layers:
@@ -222,8 +241,13 @@ fn scan_jar_for_side(data: &[u8]) -> ScanResult {
222 241
223 let mut has_server_content = false; 242 let mut has_server_content = false;
224 let mut has_client_api = false; 243 let mut has_client_api = false;
244 let mut scanned_count = 0_usize;
225 245
226 for i in 0..archive.len() { 246 for i in 0..archive.len() {
247 if scanned_count >= MAX_CLASSES_TO_SCAN {
248 tracing::warn!("Reached max class scan limit ({MAX_CLASSES_TO_SCAN}), stopping");
249 break;
250 }
227 let mut file = match archive.by_index(i) { 251 let mut file = match archive.by_index(i) {
228 Ok(f) => f, 252 Ok(f) => f,
229 Err(_) => continue, 253 Err(_) => continue,
@@ -239,6 +263,8 @@ fn scan_jar_for_side(data: &[u8]) -> ScanResult {
239 continue; 263 continue;
240 } 264 }
241 265
266 scanned_count += 1;
267
242 let result = scan_single_class(&bytes); 268 let result = scan_single_class(&bytes);
243 match result { 269 match result {
244 ClassScan::HasServerContent => has_server_content = true, 270 ClassScan::HasServerContent => has_server_content = true,
@@ -318,6 +344,9 @@ const CLIENT_API_PREFIXES: &[&[u8]] = &[
318/// We parse the constant pool minimally to find `CONSTANT_Utf8` entries, 344/// We parse the constant pool minimally to find `CONSTANT_Utf8` entries,
319/// which store the actual string literals (class names, method signatures, etc.) 345/// which store the actual string literals (class names, method signatures, etc.)
320/// used by the bytecode. This avoids false positives from raw byte scanning. 346/// used by the bytecode. This avoids false positives from raw byte scanning.
347/// Maximum constant pool entries to scan in a single class file (prevents crafted-file DoS).
348const MAX_CP_ENTRIES: usize = 10_000;
349
321fn scan_single_class(data: &[u8]) -> ClassScan { 350fn scan_single_class(data: &[u8]) -> ClassScan {
322 if data.len() < 10 { 351 if data.len() < 10 {
323 return ClassScan::Neither; 352 return ClassScan::Neither;
@@ -327,7 +356,7 @@ fn scan_single_class(data: &[u8]) -> ClassScan {
327 } 356 }
328 357
329 let cp_count = read_u16_be(&data[8..10]) as usize; 358 let cp_count = read_u16_be(&data[8..10]) as usize;
330 if cp_count < 1 { 359 if !(1..=MAX_CP_ENTRIES).contains(&cp_count) {
331 return ClassScan::Neither; 360 return ClassScan::Neither;
332 } 361 }
333 362