diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-29 02:24:41 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-29 02:24:41 +0200 |
| commit | c654a6d17eb58adbb15b9c47ab32bcc3a1666307 (patch) | |
| tree | 545544bf7d805254415da787fad1cdc711767833 /src/routes/backups.rs | |
| parent | Add per-instance mod management with client-side detection (diff) | |
| download | minecraft-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.rs | 40 |
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 | ||
| 13 | use crate::auth::CurrentUser; | 13 | use crate::auth::CurrentUser; |
| 14 | use crate::routes::AppState; | 14 | use crate::routes::AppState; |
| 15 | use 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()) |
