summaryrefslogtreecommitdiffstats
path: root/src/routes/backups.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/routes/backups.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/routes/backups.rs40
1 files changed, 28 insertions, 12 deletions
diff --git a/src/routes/backups.rs b/src/routes/backups.rs
index d68be8d..b55d5c8 100644
--- a/src/routes/backups.rs
+++ b/src/routes/backups.rs
@@ -12,6 +12,7 @@ use zip::write::FileOptions;
12 12
13use crate::auth::CurrentUser; 13use crate::auth::CurrentUser;
14use crate::routes::AppState; 14use crate::routes::AppState;
15use crate::sanitize;
15 16
16#[derive(Template)] 17#[derive(Template)]
17#[template(path = "backups/list.html")] 18#[template(path = "backups/list.html")]
@@ -97,20 +98,37 @@ async fn create_backup(
97 .map_err(|_| Redirect::to("/instances"))?; 98 .map_err(|_| Redirect::to("/instances"))?;
98 drop(conn); 99 drop(conn);
99 100
101 // Sanitize instance name to prevent path traversal
102 let safe_name = match sanitize::sanitize_instance_name(&instance_name) {
103 Some(n) => n,
104 None => return Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))),
105 };
106
100 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); 107 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
101 let filename = format!("{instance_name}_{timestamp}.zip"); 108 let filename = format!("{safe_name}_{timestamp}.zip");
102 109
103 let data_dir = state 110 let data_dir = state
104 .config 111 .config
105 .server_directory 112 .server_directory
106 .join("instances") 113 .join("instances")
107 .join(&instance_name) 114 .join(&safe_name)
108 .join("data"); 115 .join("data");
109 116
110 let backup_dir = state.config.server_directory.join("backups"); 117 let backup_dir = state.config.server_directory.join("backups");
111 let _ = std::fs::create_dir_all(&backup_dir); 118 let _ = std::fs::create_dir_all(&backup_dir);
112 let backup_path = backup_dir.join(&filename); 119 let backup_path = backup_dir.join(&filename);
113 120
121 // FIX: Insert DB record BEFORE creating the ZIP file.
122 // This prevents orphaned backup files if the process crashes between ZIP creation and DB insert.
123 let conn = state.db.conn();
124 conn.execute(
125 "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)",
126 params![instance_id, filename],
127 )
128 .ok();
129 drop(conn);
130
131 // Now create the ZIP file (DB insert already succeeded)
114 if let Ok(file) = std::fs::File::create(&backup_path) { 132 if let Ok(file) = std::fs::File::create(&backup_path) {
115 let mut zip = zip::ZipWriter::new(file); 133 let mut zip = zip::ZipWriter::new(file);
116 let options: FileOptions<'_, ()> = 134 let options: FileOptions<'_, ()> =
@@ -130,14 +148,6 @@ async fn create_backup(
130 let _ = zip.finish(); 148 let _ = zip.finish();
131 } 149 }
132 150
133 let conn = state.db.conn();
134 conn.execute(
135 "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)",
136 params![instance_id, filename],
137 )
138 .ok();
139 drop(conn);
140
141 Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) 151 Ok(Redirect::to(&format!("/instances/{instance_id}/backups")))
142} 152}
143 153
@@ -174,11 +184,17 @@ async fn download_backup(
174 .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?; 184 .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?;
175 drop(conn); 185 drop(conn);
176 186
187 // Sanitize backup filename to prevent path traversal
188 let safe_filename = match sanitize::sanitize_backup_filename(&filename) {
189 Some(f) => f,
190 None => return Err((StatusCode::BAD_REQUEST, "Invalid backup filename")),
191 };
192
177 let backup_path = state 193 let backup_path = state
178 .config 194 .config
179 .server_directory 195 .server_directory
180 .join("backups") 196 .join("backups")
181 .join(&filename); 197 .join(&safe_filename);
182 198
183 let content = 199 let content =
184 std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?; 200 std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?;
@@ -187,7 +203,7 @@ async fn download_backup(
187 .header(header::CONTENT_TYPE, "application/zip") 203 .header(header::CONTENT_TYPE, "application/zip")
188 .header( 204 .header(
189 header::CONTENT_DISPOSITION, 205 header::CONTENT_DISPOSITION,
190 format!("attachment; filename=\"{filename}\""), 206 format!("attachment; filename=\"{safe_filename}\""),
191 ) 207 )
192 .body(Body::from(content)) 208 .body(Body::from(content))
193 .unwrap()) 209 .unwrap())