From c654a6d17eb58adbb15b9c47ab32bcc3a1666307 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 29 Jul 2026 02:24:41 +0200 Subject: 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. --- src/routes/players.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'src/routes/players.rs') diff --git a/src/routes/players.rs b/src/routes/players.rs index 7cfb722..f90113a 100644 --- a/src/routes/players.rs +++ b/src/routes/players.rs @@ -9,6 +9,7 @@ use serde::Deserialize; use crate::auth::CurrentUser; use crate::routes::AppState; +use crate::sanitize; #[derive(Template)] #[template(path = "players/manage.html")] @@ -46,11 +47,14 @@ async fn manage_players( let (instance_name, whitelist, ops, bans) = if let Some((_id, inst_name, _rcon_pw, _container_state)) = active { + // Sanitize instance name to prevent path traversal in file reads + let safe_name = sanitize::sanitize_instance_name(&inst_name) + .unwrap_or_else(|| inst_name.clone()); let data_dir = state .config .server_directory .join("instances") - .join(&inst_name) + .join(&safe_name) .join("data"); let w = read_json_list(&data_dir.join("whitelist.json")); @@ -92,9 +96,20 @@ macro_rules! player_action { CurrentUser(_user): CurrentUser, Form(form): Form, ) -> Result { + // Validate Minecraft username to prevent RCON command injection + // Minecraft usernames are 3-16 alphanumeric + underscore chars + let safe_username = form.username.trim(); + if !sanitize::validate_username(safe_username) { + tracing::warn!( + "Rejected invalid username for '{}': {safe_username:?}", + stringify!($name) + ); + return Ok(Redirect::to("/players")); + } + if let Some((_, _, rcon_pw, _)) = get_active_instance(&state.db) { let client = crate::rcon::RconClient::new(rcon_pw); - let cmd = format!($command, form.username); + let cmd = format!($command, safe_username); match client.send_command_async(cmd.clone()).await { Ok(resp) => tracing::info!("RCON '{}' -> {}", cmd, resp.trim()), Err(e) => tracing::error!("RCON '{}' failed: {}", cmd, e), -- cgit v1.2.3