diff options
Diffstat (limited to '')
| -rw-r--r-- | piolium/attack-surface/manual-attack-surface-inventory.md | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/piolium/attack-surface/manual-attack-surface-inventory.md b/piolium/attack-surface/manual-attack-surface-inventory.md new file mode 100644 index 0000000..c1793e4 --- /dev/null +++ b/piolium/attack-surface/manual-attack-surface-inventory.md | |||
| @@ -0,0 +1,311 @@ | |||
| 1 | # Manual Attack Surface Inventory — Phase 8 Deep Probe | ||
| 2 | |||
| 3 | **Target:** salmon/minecraft-ui | ||
| 4 | **Generated:** 2025-07-29 | ||
| 5 | **Phase:** P8 — Manual Attack Surface Probe | ||
| 6 | **Scope:** All Rust source files (16 files), templates, config | ||
| 7 | |||
| 8 | --- | ||
| 9 | |||
| 10 | ## 1. Public Routes / URLs (No Auth Required) | ||
| 11 | |||
| 12 | | Method | Path | Handler | Source File:Line | Input Sources | | ||
| 13 | |--------|------|---------|-----------------|---------------| | ||
| 14 | | GET | `/login` | `login_page` | src/routes/mod.rs:53 | None (static template) | | ||
| 15 | | POST | `/login` | `authenticate` | src/auth.rs:34 | `username`, `password` (Form) | | ||
| 16 | | POST | `/logout` | `logout` | src/auth.rs:130 | Cookie header (Cookie: mcui_session=...) | | ||
| 17 | | GET | `/static/*` | `ServeDir::new("static")` | src/routes/mod.rs:28 | File path after `/static/` | | ||
| 18 | |||
| 19 | ## 2. Authenticated Routes (All Behind `require_auth`) | ||
| 20 | |||
| 21 | | Method | Path | Handler | File:Line | Key Inputs | Sink | | ||
| 22 | |--------|------|---------|-----------|------------|------| | ||
| 23 | | GET | `/` | `dashboard` | src/routes/dashboard.rs:19 | CurrentUser extension | SQL SELECT | | ||
| 24 | | GET | `/instances` | `list_instances` | src/routes/instances.rs:44 | None | SQL SELECT | | ||
| 25 | | GET | `/instances/new` | `create_form` | src/routes/instances.rs:70 | None | Outbound HTTP (Mojang API) | | ||
| 26 | | POST | `/instances/new` | `create_instance` | src/routes/instances.rs:87 | `name`, `version`, `seed`, `motd`, etc. | SQL INSERT, `create_dir_all`, RCON password gen | | ||
| 27 | | GET | `/instances/{id}/edit` | `edit_form` | src/routes/instances.rs:129 | Path `id` (i64) | SQL SELECT, `read_instance_seed`, RCON `seed` cmd | | ||
| 28 | | POST | `/instances/{id}/edit` | `edit_instance` | src/routes/instances.rs:176 | Path `id`, Form: difficulty, gamemode, motd | SQL UPDATE, `stop_container` | | ||
| 29 | | POST | `/instances/{id}/delete` | `delete_instance` | src/routes/instances.rs:198 | Path `id` | SQL UPDATE/DELETE, `remove_dir_all` | | ||
| 30 | | POST | `/instances/{id}/activate` | `activate_instance` | src/routes/instances.rs:217 | Path `id` | Docker `start_container`, RCON polling | | ||
| 31 | | POST | `/instances/deactivate` | `deactivate_instance` | src/routes/instances.rs:278 | None | Docker `stop_container` | | ||
| 32 | | GET | `/instances/{id}/world/export` | `export_world` | src/routes/instances.rs:288 | Path `id` | `read_dir`, `ZipWriter`, file read | | ||
| 33 | | POST | `/instances/{id}/world/import` | `import_world` | src/routes/instances.rs:328 | Path `id`, Multipart ZIP | `ZipArchive`, `File::create`, `remove_dir_all` | | ||
| 34 | | POST | `/instances/{id}/world/regenerate` | `regenerate_world` | src/routes/instances.rs:360 | Path `id`, Form: `seed` | `read_to_string`, `write`, `remove_dir_all` | | ||
| 35 | | GET | `/players` | `manage_players` | src/routes/players.rs:28 | None | `read_to_string` (whitelist/ops/bans JSON) | | ||
| 36 | | POST | `/players/whitelist/add` | `add_whitelist` | src/routes/players.rs:89 | `username` (Form) | RCON `send_command_async` | | ||
| 37 | | POST | `/players/whitelist/remove` | `remove_whitelist` | src/routes/players.rs:89 | `username` | RCON `send_command_async` | | ||
| 38 | | POST | `/players/op/add` | `add_op` | src/routes/players.rs:89 | `username` | RCON `send_command_async` | | ||
| 39 | | POST | `/players/op/remove` | `remove_op` | src/routes/players.rs:89 | `username` | RCON `send_command_async` | | ||
| 40 | | POST | `/players/ban/add` | `add_ban` | src/routes/players.rs:89 | `username` | RCON `send_command_async` | | ||
| 41 | | POST | `/players/ban/remove` | `remove_ban` | src/routes/players.rs:89 | `username` | RCON `send_command_async` | | ||
| 42 | | GET | `/instances/{id}/mods` | `list_mods` | src/routes/mods.rs:42 | Path `id` | SQL SELECT | | ||
| 43 | | POST | `/instances/{id}/mods` | `upload_mod` | src/routes/mods.rs:86 | Path `id`, Multipart .jar/.zip | `std::fs::write`, JAR parsing, SQL INSERT | | ||
| 44 | | POST | `/instances/{id}/mods/{mod_id}/delete` | `delete_mod` | src/routes/mods.rs:188 | Path `instance_id`, `mod_id` | `remove_file`, SQL DELETE | | ||
| 45 | | POST | `/instances/{id}/mods/rescan` | `rescan_mods` | src/routes/mods.rs:163 | Path `id` | `read_dir`, `std::fs::read`, JAR parsing | | ||
| 46 | | GET | `/instances/{id}/backups` | `list_backups` | src/routes/backups.rs:30 | Path `id` | SQL SELECT | | ||
| 47 | | POST | `/instances/{id}/backups/create` | `create_backup` | src/routes/backups.rs:54 | Path `id` | `File::create`, `ZipWriter`, SQL INSERT | | ||
| 48 | | POST | `/backups/{id}/restore` | `restore_backup` | src/routes/backups.rs:88 | Path `id` | SQL SELECT (STUB — no-op) | | ||
| 49 | | GET | `/backups/{id}/download` | `download_backup` | src/routes/backups.rs:93 | Path `id` | SQL SELECT, `std::fs::read` | | ||
| 50 | | GET | `/accounts` | `manage_accounts` | src/routes/accounts.rs:60 | CurrentUser (must be superuser) | SQL SELECT | | ||
| 51 | | POST | `/accounts/create` | `create_account` | src/routes/accounts.rs:95 | `username`, `password`, `is_superuser` | SQL INSERT | | ||
| 52 | | POST | `/accounts/delete` | `delete_account` | src/routes/accounts.rs:111 | `form.id` | SQL DELETE | | ||
| 53 | | POST | `/accounts/set-password` | `set_password` | src/routes/accounts.rs:142 | `form.id`, `new_password` | SQL UPDATE, session DELETE | | ||
| 54 | | GET | `/change-password` | `change_password_page` | src/routes/mod.rs:58 | CurrentUser | None | | ||
| 55 | | POST | `/change-password` | `change_password` | src/routes/accounts.rs:163 | `current_password`, `new_password` | SQL UPDATE, session DELETE | | ||
| 56 | |||
| 57 | --- | ||
| 58 | |||
| 59 | ## 3. Attacker-Controlled Input Sources (By Category) | ||
| 60 | |||
| 61 | ### 3a. Form Fields (application/x-www-form-urlencoded) | ||
| 62 | | Field | Handler(s) | Max Size | Validation | Risk | | ||
| 63 | |-------|-----------|----------|------------|------| | ||
| 64 | | `username` | authenticate, player actions, create_account, set_password | ~64KB | None (SQL param prevents injection) | LOW (SQL safe), **HIGH** (RCON injection in player actions) | | ||
| 65 | | `password` | authenticate, change_password, create_account | ~64KB | Argon2id hashed | LOW | | ||
| 66 | | `name` | create_instance | ~64KB | **NONE** | **CRITICAL** — used in fs::create_dir_all, stored in DB, used in remove_dir_all | | ||
| 67 | | `seed` | create_instance, regenerate_world | ~64KB | **NONE** — written to server.properties | **MEDIUM** — properties injection | | ||
| 68 | | `motd` | create_instance, edit_instance | ~64KB | **NONE** — passed to Docker env | **HIGH** — Docker env injection | | ||
| 69 | | `version` | create_instance | ~64KB | **NONE** — passed to Docker env | MEDIUM | | ||
| 70 | | `difficulty` / `gamemode` / `max_players` | create_instance, edit_instance | i32 | Range limited | MEDIUM | | ||
| 71 | | `memory_mb` | create_instance, edit_instance | i32 | 512..max_memory_mb | LOW | | ||
| 72 | | `server_type` | create_instance | ~64KB | CHECK constraint in SQL | LOW (DB enforced) | | ||
| 73 | | `level_type` | create_instance | ~64KB | Default "DEFAULT" | MEDIUM — Docker env | | ||
| 74 | | `pvp` | create_instance, edit_instance | ~8 chars | Checkbox (Some("on") | LOW | | ||
| 75 | | `world_source` | create_instance | ~64KB | CHECK constraint in SQL | LOW | | ||
| 76 | |||
| 77 | ### 3b. Multipart File Uploads | ||
| 78 | | Field | Handler | Max Size | Validation | Risk | | ||
| 79 | |-------|---------|----------|------------|------| | ||
| 80 | | File bytes (.jar) | upload_mod | 500MB per field | JAR metadata validation + class scan | **HIGH** — file write | | ||
| 81 | | File bytes (.zip) | upload_mod | 500MB per field | ZIP → extract .jar → JAR validation | **HIGH** — file write, nested ZIP | | ||
| 82 | | File bytes (.zip) | import_world | Multipart stream | `enclosed_name()` check | **HIGH** — world file write, zip slip edge cases | | ||
| 83 | | Filename (from Content-Disposition) | upload_mod | ~255 chars | `file_name()` strips dir components | **MEDIUM** — special chars preserved | | ||
| 84 | | Content-Type / boundary | upload_mod | ~255 chars | **NONE** — directly parsed for multer | **LOW** — multer handles internally | | ||
| 85 | |||
| 86 | ### 3c. Path Parameters | ||
| 87 | | Parameter | Handler(s) | Validation | Risk | | ||
| 88 | |-----------|-----------|------------|------| | ||
| 89 | | `id` (i64) | All /instances/{id}/*, /backups/{id}/* | Axum Path<i64> (integer only) | LOW — SQL uses params | | ||
| 90 | | `instance_id` (i64) | delete_mod | Axum Path<(i64, i64)> | LOW | | ||
| 91 | | `mod_id` (i64) | delete_mod | Axum Path<(i64, i64)> | LOW | | ||
| 92 | |||
| 93 | ### 3d. Cookie / Header Sources | ||
| 94 | | Source | Handler | What It Provides | Risk | | ||
| 95 | |--------|---------|-----------------|------| | ||
| 96 | | `Cookie: mcui_session` | require_auth (all protected routes) | Session ID → Account | **MEDIUM** — no server-side TTL | | ||
| 97 | | `Cookie: mcui_session` | logout | Session ID for deletion | **LOW** — missing auth guard | | ||
| 98 | | `Content-Type: boundary=` | upload_mod | Multipart parsing boundary | **LOW** | | ||
| 99 | |||
| 100 | --- | ||
| 101 | |||
| 102 | ## 4. Sinks (Dangerous Operations) | ||
| 103 | |||
| 104 | ### 4a. Filesystem Write Sinks | ||
| 105 | | Sink | File:Line | Data Source | Traversal Protection | | ||
| 106 | |------|-----------|-------------|---------------------| | ||
| 107 | | `std::fs::create_dir_all(&instance_dir)` | src/routes/instances.rs:205 | `form.name` (user input) | **NONE** — `Path::join` traverses on `..` | | ||
| 108 | | `std::fs::write(&dest, bytes)` | src/routes/mods.rs:98 | `safe_name` (filename from upload) | Partial — `file_name()` strips dir | | ||
| 109 | | `std::fs::File::create(&out_path)` | src/routes/instances.rs:210 | `file.enclosed_name()` (ZIP entry path) | Partial — `enclosed_name()` checks | | ||
| 110 | | `std::fs::File::create(&backup_path)` | src/routes/backups.rs:72 | `filename` (timestamp-based) | LOW — controlled by app | | ||
| 111 | | `std::fs::write(&props_path, ...)` | src/routes/instances.rs:396 | `form.seed` (user input) | **NONE** — content injection | | ||
| 112 | | `std::fs::remove_dir_all(&instance_dir)` | src/routes/instances.rs:392-393 | `name` from DB (originally user input) | **NONE** | | ||
| 113 | | `std::fs::remove_dir_all(&world_dir)` | src/routes/instances.rs:421 | Instance name from DB | Partial | | ||
| 114 | | `std::fs::remove_file(&mod_path)` | src/routes/mods.rs:226 | `filename` from DB | Partial | | ||
| 115 | |||
| 116 | ### 4b. Filesystem Read Sinks | ||
| 117 | | Sink | File:Line | Data Source | Risk | | ||
| 118 | |------|-----------|-------------|------| | ||
| 119 | | `std::fs::read(&backup_path)` | src/routes/backups.rs:106 | `filename` from DB | **MEDIUM** — filename from DB could be traversed | | ||
| 120 | | `std::fs::read_dir(&data_dir)` | src/routes/backups.rs:68 | Instance name from DB | LOW | | ||
| 121 | | `std::fs::read_to_string(&props_path)` | src/routes/instances.rs:386 | Instance name from DB | LOW | | ||
| 122 | | `std::fs::read_dir(&mods_dir)` | src/routes/mods.rs:175 | Instance name from DB | LOW | | ||
| 123 | | `std::fs::read(&path)` | src/routes/mods.rs:183 | File path from read_dir | LOW | | ||
| 124 | |||
| 125 | ### 4c. RCON Command Sinks | ||
| 126 | | Sink | File:Line | Data Source | Command Template | Risk | | ||
| 127 | |------|-----------|-------------|-----------------|------| | ||
| 128 | | `client.send_command_async(cmd)` | src/routes/players.rs:97 | `form.username` | `format!("whitelist add {}", form.username)` | **HIGH** — command injection | | ||
| 129 | | `client.send_command_async(cmd)` | src/routes/players.rs:97 | `form.username` | `format!("op {}", form.username)` | **HIGH** | | ||
| 130 | | `client.send_command_async(cmd)` | src/routes/players.rs:97 | `form.username` | `format!("ban {}", form.username)` | **HIGH** | | ||
| 131 | | `client.send_command_async("seed".into())` | src/routes/instances.rs:153 | Hardcoded "seed" | `"seed"` | LOW | | ||
| 132 | | `client.send_command_async("list".into())` | src/routes/instances.rs:251 | Hardcoded "list" | `"list"` | LOW | | ||
| 133 | |||
| 134 | ### 4d. Docker Control Sinks | ||
| 135 | | Sink | File:Line | Data Source | Risk | | ||
| 136 | |------|-----------|-------------|------| | ||
| 137 | | `docker.start_container(&instance)` | src/routes/instances.rs:235 | Full Instance struct | **HIGH** — all settings go to Docker env | | ||
| 138 | | `docker.stop_container()` | src/routes/instances.rs:195 | None | LOW | | ||
| 139 | | `recreate_container()` | src/docker.rs:62 | `instance.name`, `instance.motd`, all settings | **HIGH** — env injection | | ||
| 140 | | `env_strings` construction | src/docker.rs:87-113 | `instance.version`, `instance.motd`, `instance.rcon_password`, etc. | **HIGH** — no escaping | | ||
| 141 | |||
| 142 | ### 4e. SQL Query Sinks | ||
| 143 | All SQL queries use `rusqlite::params![]` macro which provides parameterized queries. SQL injection is not feasible. | ||
| 144 | |||
| 145 | ### 4f. Template Rendering Sinks | ||
| 146 | | Sink | File:Line | Data | Risk | | ||
| 147 | |------|-----------|------|------| | ||
| 148 | | `template.render().unwrap_or_default()` | Various | User-controlled strings (name, motd, etc.) | **LOW** — Askama is compile-time template engine, no XSS injection possible | | ||
| 149 | |||
| 150 | --- | ||
| 151 | |||
| 152 | ## 5. Hidden Control Channels | ||
| 153 | |||
| 154 | | Channel | File:Line | Processing | What It Controls | Security Concern | | ||
| 155 | |---------|-----------|-----------|-----------------|-----------------| | ||
| 156 | | `Cookie: mcui_session` value | src/auth.rs:107 | Raw `split(';')` then `split_once('=')` | Identity (session ID) | Naive cookie parsing, no OWS stripping per RFC 6265. Cookie value with `=` or `;` could confuse parsing | | ||
| 157 | | `Content-Type: boundary=` | src/routes/mods.rs:141-148 | Custom parsing: `split(';')` → `strip_prefix("boundary=")` → `trim_matches('"')` | Multer multipart boundary | No validation on boundary value; multer validates internally but custom extraction may produce empty boundary | | ||
| 158 | | Instance properties → Docker env | src/docker.rs:87-113 | Direct `format!()` calls | Docker container environment | **No escaping** — newlines in MOTD or seed can inject arbitrary env vars | | ||
| 159 | | Instance name → filesystem path | src/routes/instances.rs:205 | `server_directory.join("instances").join(&form.name)` | Filesystem paths | **No sanitization** — `..` in name causes directory traversal | | ||
| 160 | | RCON password from DB → rendered HTML | src/routes/instances.rs:313 | `row.get(15)` (rcon_password) | Exposed to all authenticated users | Any authenticated user can see the RCON password for any instance | | ||
| 161 | | Seed → server.properties | src/routes/instances.rs:386-397 | Direct `format!("level-seed={seed}")` | server.properties content | **No escaping** — can inject arbitrary properties lines | | ||
| 162 | | Username → RCON command | src/routes/players.rs:93-97 | `format!("whitelist add {}", form.username)` | Minecraft server state | **No sanitization** — can inject arbitrary RCON commands | | ||
| 163 | |||
| 164 | --- | ||
| 165 | |||
| 166 | ## 6. Exploit-Relevant Paths (Verified with Code Evidence) | ||
| 167 | |||
| 168 | ### X1: Instance Name Path Traversal → Arbitrary Directory Write | ||
| 169 | ``` | ||
| 170 | POST /instances/new → CreateInstanceForm.name = "../../../tmp/evil" | ||
| 171 | → src/routes/instances.rs:205: std::fs::create_dir_all(server_dir/instances/../../../tmp/evil/data) | ||
| 172 | → SQL INSERT at line 119 with name stored as-is | ||
| 173 | → Later: GET /instances/{id}/edit → edit_form reads name from DB | ||
| 174 | → Later: POST /instances/{id}/delete → remove_dir_all(server_dir/instances/../../../tmp/evil) | ||
| 175 | ``` | ||
| 176 | **Evidence:** `Path::join` on user-controlled `form.name` at line 173. No sanitization on name before `create_dir_all` at line 205. | ||
| 177 | |||
| 178 | ### X2: RCON Command Injection via Player Username | ||
| 179 | ``` | ||
| 180 | POST /players/op/add → PlayerAction.username = "attacker; op injected_player" | ||
| 181 | → src/routes/players.rs:93: format!("op {}", form.username) | ||
| 182 | → src/routes/players.rs:97: client.send_command_async("op attacker; op injected_player") | ||
| 183 | ``` | ||
| 184 | **Evidence:** `player_action!` macro at line 89 generates direct format string. No sanitization, no validation of username characters. | ||
| 185 | |||
| 186 | ### X3: MOTD Docker Environment Injection | ||
| 187 | ``` | ||
| 188 | POST /instances/{id}/edit → EditInstanceForm.motd = "hello\nEULA=FALSE\nONLINE_MODE=true" | ||
| 189 | → SQL UPDATE at line 186 (stored in DB) | ||
| 190 | → POST /instances/{id}/activate → reads Instance struct | ||
| 191 | → src/docker.rs:103: format!("MOTD={}", instance.motd) → env_strings.push("MOTD=hello\nEULA=FALSE\nONLINE_MODE=true") | ||
| 192 | → Docker container env includes injected vars | ||
| 193 | ``` | ||
| 194 | **Evidence:** `format!("MOTD={}", instance.motd)` at src/docker.rs:103. No escaping of newlines or special characters. | ||
| 195 | |||
| 196 | ### X4: Level-Seed Injection into server.properties | ||
| 197 | ``` | ||
| 198 | POST /instances/{id}/world/regenerate → RegenerateWorldForm.seed = "123\nlevel-type=FLAT\ngamerule=doDaylightCycle=false\n" | ||
| 199 | → src/routes/instances.rs:397: format!("level-seed={seed}") | ||
| 200 | → Written to server.properties via line replacement (line 390-393) | ||
| 201 | → Injected lines become new properties | ||
| 202 | ``` | ||
| 203 | **Evidence:** Line 397: `new_lines.push(format!("level-seed={seed}"));` No escaping. Java `.properties` format treats newline-separated lines as separate entries. | ||
| 204 | |||
| 205 | ### X5: Mod Filename Path Traversal (Partially Mitigated) | ||
| 206 | ``` | ||
| 207 | POST /instances/{id}/mods → multipart file with filename "../../../tmp/evil.jar" | ||
| 208 | → src/routes/mods.rs:98: process_jar_upload → Path::new(filename).file_name() | ||
| 209 | → "evil.jar" preserved, "../.." stripped by file_name() | ||
| 210 | → Mitigation: streps directory components BUT unsafe write to mods_dir/evil.jar | ||
| 211 | ``` | ||
| 212 | **Evidence:** `safe_name` derived from `file_name()` at line 96, then `mods_dir.join(&safe_name)` at line 98. | ||
| 213 | |||
| 214 | ### X6: No Server-Side Session TTL | ||
| 215 | ``` | ||
| 216 | POST /login → sets cookie Max-Age=86400 (24h) | ||
| 217 | src/auth.rs:108-115: require_auth middleware executes: | ||
| 218 | "SELECT ... FROM sessions s JOIN accounts a ON s.account_id = a.id | ||
| 219 | WHERE s.id = ?1" | ||
| 220 | No "AND s.created_at > datetime('now', '-1 day')" filter | ||
| 221 | → Sessions persist indefinitely in DB | ||
| 222 | ``` | ||
| 223 | **Evidence:** Session query at src/auth.rs:113 has no time-based filter. Session ID from leaked cookie works forever. | ||
| 224 | |||
| 225 | ### X7: No CSRF Protection on Any POST Endpoint | ||
| 226 | ``` | ||
| 227 | All POST handlers: No token validation, no origin/referer check, no SameSite=Strict | ||
| 228 | → CSRF on: /logout, /instances/new, /instances/{id}/delete, /players/op/add, etc. | ||
| 229 | ``` | ||
| 230 | **Evidence:** No CSRF middleware in src/routes/mod.rs. Cookie has SameSite=Lax (not Strict). All POST operations lack CSRF tokens. | ||
| 231 | |||
| 232 | ### X8: POST /logout Missing Auth Guard | ||
| 233 | ``` | ||
| 234 | POST /logout registered on outer Router NOT behind require_auth | ||
| 235 | → src/auth.rs:130-152: Reads Cookie header, extracts session ID, DELETEs from DB | ||
| 236 | → No authentication required for the request | ||
| 237 | ``` | ||
| 238 | **Evidence:** src/routes/mod.rs:20 registers logout on outer Router, before `.merge(protected)` at line 33. | ||
| 239 | |||
| 240 | ### X9: RCON Password Exposed to All Authenticated Users | ||
| 241 | ``` | ||
| 242 | GET /instances/{id}/edit → edit_form handler | ||
| 243 | → src/routes/instances.rs:313: rcon_password: row.get(15)? | ||
| 244 | → Rendered in HTML template (EditInstanceTemplate) | ||
| 245 | → Any authenticated user can see RCON password for any instance | ||
| 246 | ``` | ||
| 247 | **Evidence:** SELECT query at line 301 includes `rcon_password`. No superuser or ownership check before rendering. | ||
| 248 | |||
| 249 | ### X10: Weak RCON Password Generation | ||
| 250 | ``` | ||
| 251 | src/routes/instances.rs:523-529: generate_rcon_password() | ||
| 252 | → rand::thread_rng() (not OsRng/crypto RNG) | ||
| 253 | → 36^16 space (lowercase alphanumeric) | ||
| 254 | ``` | ||
| 255 | **Evidence:** `rand::thread_rng()` at line 524 is not cryptographically secure. 62^16 would be stronger. | ||
| 256 | |||
| 257 | ### X11: Docker Env Variables Set Without Sanitization (Multiple) | ||
| 258 | ALL instance fields injected into Docker container env without sanitization: | ||
| 259 | - `VERSION={version}` (src/docker.rs:93) | ||
| 260 | - `MAX_PLAYERS={max_players}` (src/docker.rs:95) | ||
| 261 | - `DIFFICULTY={difficulty}` (src/docker.rs:96) | ||
| 262 | - `RCON_PASSWORD={rcon_password}` (src/docker.rs:109) | ||
| 263 | - `MOTD={motd}` (src/docker.rs:103) | ||
| 264 | - `LEVEL_TYPE={level_type}` (src/docker.rs:111) | ||
| 265 | - `LEVEL_SEED={seed}` (src/docker.rs:116) | ||
| 266 | - `GAMEMODE={gamemode}` (src/docker.rs:112) | ||
| 267 | |||
| 268 | **Evidence:** src/docker.rs:87-116 — All `format!()` calls with direct `instance.*` values. No escaping. | ||
| 269 | |||
| 270 | ### X12: Backup Restore is No-Op (Data Loss Risk) | ||
| 271 | ``` | ||
| 272 | POST /backups/{id}/restore → restore_backup handler | ||
| 273 | → src/routes/backups.rs:88-96: Looks up backup in DB → redirects | ||
| 274 | → NO actual file restoration performed | ||
| 275 | ``` | ||
| 276 | **Evidence:** Handler at line 88 does SELECT then returns Redirect. No file copy/restore operations. | ||
| 277 | |||
| 278 | ### X13: Mod Validation DoS via Crafted JAR | ||
| 279 | ``` | ||
| 280 | POST /instances/{id}/mods → upload crafted JAR with malicious constant pool | ||
| 281 | → src/mods.rs:120: ZipArchive::new(data) | ||
| 282 | → src/mods.rs:185: scan_single_class(data) — walks constant pool | ||
| 283 | → No timeout, no recursion depth limit, no size limits on class files | ||
| 284 | → Crafted constant pool (e.g., giant CONSTANT_Utf8 entries) could cause OOM | ||
| 285 | ``` | ||
| 286 | **Evidence:** Loop at line 173 (`while i < cp_count`) has no iteration limit beyond cp_count. No bounds on CONSTANT_Utf8 length. | ||
| 287 | |||
| 288 | ### X14: Recursive ZIP Extraction Without Depth Limit | ||
| 289 | ``` | ||
| 290 | POST /instances/{id}/mods → upload ZIP containing JAR (which is also a ZIP) | ||
| 291 | → src/routes/mods.rs:131: process_zip_upload → extracts JAR bytes | ||
| 292 | → src/routes/mods.rs:149: process_jar_upload → validate_mod | ||
| 293 | → src/mods.rs:120: ZipArchive::new(data) — re-opens JAR as ZIP | ||
| 294 | → No depth counter; possible infinite recursion | ||
| 295 | ``` | ||
| 296 | **Evidence:** process_zip_upload calls process_jar_upload which calls validate_mod which opens the JAR as ZIP. No depth tracking. | ||
| 297 | |||
| 298 | --- | ||
| 299 | |||
| 300 | ## 7. Trust Boundary Crossings | ||
| 301 | |||
| 302 | | Boundary | Crossing Point | Direction | Data | Protection | | ||
| 303 | |----------|---------------|-----------|------|------------| | ||
| 304 | | HTTP → Auth | require_auth middleware (src/auth.rs:55) | Inbound | Session cookie → Account | Session lookup in DB | | ||
| 305 | | Auth → Instance Data | All protected handlers | Inbound | Authenticated request → CRUD | None (flat auth model) | | ||
| 306 | | WebUI → Filesystem | create_dir_all, fs::write, File::create | Outbound | User-controlled name/file content | Partial (mod filename) | | ||
| 307 | | WebUI → Docker | docker::start_container | Outbound | Instance settings | **NONE** | | ||
| 308 | | WebUI → RCON | RconClient::send_command_async | Outbound | Player username, commands | **NONE** | | ||
| 309 | | WebUI → Database | rusqlite execute/query | Outbound | Form data | Parameterized queries | | ||
| 310 | | WebUI → HTTP (Mojang) | reqwest::Client::get | Outbound | None (hardcoded URL) | Trusted URL | | ||
| 311 | | Upload → Validation | validate_mod (src/mods.rs:99) | Internal | Raw JAR bytes | Metadata + class scan | | ||
