diff options
Diffstat (limited to '')
| -rw-r--r-- | piolium/attack-surface/knowledge-base-report.md | 597 |
1 files changed, 597 insertions, 0 deletions
diff --git a/piolium/attack-surface/knowledge-base-report.md b/piolium/attack-surface/knowledge-base-report.md new file mode 100644 index 0000000..6a0b502 --- /dev/null +++ b/piolium/attack-surface/knowledge-base-report.md | |||
| @@ -0,0 +1,597 @@ | |||
| 1 | # Phase 3 Knowledge Base Report | ||
| 2 | |||
| 3 | **Target:** salmon/minecraft-ui | ||
| 4 | **Generated:** 2025-07-29 | ||
| 5 | **Phase:** P3 (Architecture & Threat Model) | ||
| 6 | **Depth:** deep | ||
| 7 | |||
| 8 | --- | ||
| 9 | |||
| 10 | ## Project Classification | ||
| 11 | |||
| 12 | **Type:** Web App (self-hosted, single-user/small-team Minecraft server manager) | ||
| 13 | |||
| 14 | **Subtypes:** | ||
| 15 | - HTTP server (axum, localhost-bound) | ||
| 16 | - Docker orchestrator (bollard → Docker daemon → itzg/minecraft-server containers) | ||
| 17 | - File upload handler (multipart mod uploads, world ZIP imports) | ||
| 18 | - Database-backed CRUD app (SQLite via rusqlite) | ||
| 19 | - RCON client (TCP Minecraft Remote Console protocol) | ||
| 20 | |||
| 21 | **Deployment model:** Single-process server on a Linux host with Docker daemon. | ||
| 22 | Binds to `127.0.0.1:<port>` — expected to sit behind a reverse proxy (nginx). | ||
| 23 | |||
| 24 | --- | ||
| 25 | |||
| 26 | ## Architecture Model | ||
| 27 | |||
| 28 | ### Component Inventory | ||
| 29 | |||
| 30 | | Component | Technology | Role | Trust Boundary | | ||
| 31 | |-----------|-----------|------|----------------| | ||
| 32 | | **WebUI Server** | Rust, axum 0.8.9, tokio | HTTP request handler, template rendering | Listens on localhost; proxy-facing | | ||
| 33 | | **Auth Layer** | Custom session tokens, Argon2id | Authentication + session management | Unauthenticated→authenticated boundary | | ||
| 34 | | **Database** | SQLite (rusqlite 0.40.1 bundled) | Accounts, sessions, instances, mods, backups | Single process — no network exposure | | ||
| 35 | | **Docker Manager** | bollard 0.17 → Docker daemon | Container lifecycle, image pull, port binding | Local Unix socket — host-OS trust | | ||
| 36 | | **RCON Client** | Custom TCP client (127.0.0.1:25575) | Send commands to Minecraft server | Local network — Docker container boundary | | ||
| 37 | | **Mod Validator** | Custom JAR parser | Forge/Fabric metadata analysis, class-file scan | Accept/reject before write to disk | | ||
| 38 | | **Backup Engine** | ZIP archives via `zip` crate | World/instance snapshots and restore | Filesystem write to server_directory | | ||
| 39 | | **Mojang API Client** | reqwest → `piston-meta.mojang.com` | Fetch Minecraft version manifest | Outbound HTTPS to external API | | ||
| 40 | | **Static File Server** | tower-http ServeDir | Serve `/static/` (CSS, JS) | Public — no auth | | ||
| 41 | | **Docker Container** | itzg/minecraft-server (java8/17/21) | Minecraft server runtime | RCON boundary, network port 25565/25575 | | ||
| 42 | | **Config** | TOML file (config.toml) | Server directory, port, max memory, initial superuser | Filesystem — read at startup | | ||
| 43 | |||
| 44 | ### Data Transports | ||
| 45 | |||
| 46 | 1. **HTTP** (localhost:3000) — all web traffic | ||
| 47 | 2. **Docker Unix Socket** (`/var/run/docker.sock`) — container management | ||
| 48 | 3. **TCP** (localhost:25575) — RCON protocol to Minecraft container | ||
| 49 | 4. **TCP** (0.0.0.0:25565) — Minecraft client connections (managed by container, not webUI) | ||
| 50 | 5. **HTTPS** (outbound to Mojang API) — version manifest fetch | ||
| 51 | |||
| 52 | ### Trust Boundaries | ||
| 53 | |||
| 54 | ``` | ||
| 55 | HTTP Client (attacker) ──[localhost:3000]──▶ WebUI Server | ||
| 56 | │ | ||
| 57 | ┌───── auth layer ─────┐ | ||
| 58 | ▼ ▼ | ||
| 59 | Authenticated Unauthenticated routes | ||
| 60 | routes (auth'd) (/login, /logout, /static/*) | ||
| 61 | │ | ||
| 62 | ┌──────────┼──────────┐ | ||
| 63 | ▼ ▼ ▼ | ||
| 64 | SQLite DB Docker daemon RCON → Minecraft | ||
| 65 | (mcui.db) (/var/run/docker.sock) (127.0.0.1:25575) | ||
| 66 | ``` | ||
| 67 | |||
| 68 | **Key trust boundaries:** | ||
| 69 | 1. **Public→Protected routes:** session cookie check via `require_auth` middleware (src/auth.rs:55) | ||
| 70 | 2. **WebUI→Docker:** local Unix socket — any code running as the webUI user can control containers | ||
| 71 | 3. **WebUI→RCON:** localhost TCP — password-protected, but breakable if rcon_password exposed | ||
| 72 | 4. **WebUI→Database:** same-process SQLite — no isolation between auth and data ops | ||
| 73 | 5. **Mod upload→Filesystem:** validated JAR → instance mods directory (path traversal risk) | ||
| 74 | 6. **World import→Filesystem:** ZIP extraction → world directory (path traversal risk) | ||
| 75 | |||
| 76 | --- | ||
| 77 | |||
| 78 | ## DFD/CFD Slices | ||
| 79 | |||
| 80 | ### DFD Slice 1: Unauthenticated Login Flow | ||
| 81 | |||
| 82 | ``` | ||
| 83 | ┌─────────┐ POST /login ┌──────────────┐ SELECT ┌─────────┐ | ||
| 84 | │Attacker │ username, │ WebUI Auth │────────▶ │ SQLite │ | ||
| 85 | │(browser)│ password │ (auth.rs) │ │accounts │ | ||
| 86 | └─────────┘ │ │◀──────── │ table │ | ||
| 87 | │ │ verify_hash │ password └─────────┘ | ||
| 88 | │ └──────┬───────┘ | ||
| 89 | │ │ INSERT session | ||
| 90 | │ ┌──────▼───────┐ | ||
| 91 | │ │ SQLite │ | ||
| 92 | └── Set-Cookie ─────▶│ sessions │ | ||
| 93 | │ table │ | ||
| 94 | └──────────────┘ | ||
| 95 | ``` | ||
| 96 | **Security decisions:** password hashing (Argon2id), session ID generation (random UUID v4), cookie flags (HttpOnly, SameSite=Lax, Path=/, Max-Age=86400). | ||
| 97 | |||
| 98 | ### DFD Slice 2: Mod Upload (Highest Risk) | ||
| 99 | |||
| 100 | ``` | ||
| 101 | ┌─────────┐ POST /instances/{id}/mods ┌──────────────┐ | ||
| 102 | │Attacker │ multipart/form-data │ WebUI │ | ||
| 103 | │(auth'd) │ .jar / .zip file │ routes/mods │ | ||
| 104 | └─────────┘ │ .rs │ | ||
| 105 | │ └──────┬───────┘ | ||
| 106 | │ multer parse (500MB limit) │ | ||
| 107 | │ zip extraction (for .zip) │ | ||
| 108 | │ ▼ | ||
| 109 | │ ┌──────────────┐ | ||
| 110 | │ │ ModValidator │ | ||
| 111 | │ │ (src/mods.rs)│ | ||
| 112 | │ │ • forge meta │ | ||
| 113 | │ │ • fabric meta │ | ||
| 114 | │ │ • class scan │ | ||
| 115 | │ └──────┬───────┘ | ||
| 116 | │ accepted=True │ | ||
| 117 | │ ┌───────────────▼────────┐ | ||
| 118 | │ │ Filesystem: │ | ||
| 119 | │ │ .../mods/<filename> │ | ||
| 120 | │ │ SQLite: mods table │ | ||
| 121 | │ └───────────────────────┘ | ||
| 122 | ``` | ||
| 123 | **Security decisions:** file extension check (`.jar`/`.zip`), metadata validation, class-file scanning. **No path traversal sanitization** on filename. | ||
| 124 | |||
| 125 | ### DFD Slice 3: World Import (Zip Slip Vector) | ||
| 126 | |||
| 127 | ``` | ||
| 128 | Attacker ──▶ POST /instances/{id}/world/import ──▶ Multipart ZIP upload | ||
| 129 | │ | ||
| 130 | zip::ZipArchive::new() | ||
| 131 | │ | ||
| 132 | for i in 0..archive.len() { | ||
| 133 | file.enclosed_name() | ||
| 134 | │ | ||
| 135 | ┌──────▼────────┐ | ||
| 136 | │ out_path = │ | ||
| 137 | │ world_dir │ | ||
| 138 | │ .join(path) │ | ||
| 139 | └──────┬────────┘ | ||
| 140 | │ | ||
| 141 | std::fs::create_dir_all(out_path) | ||
| 142 | std::fs::File::create(out_path) | ||
| 143 | ``` | ||
| 144 | **Vulnerability:** `file.enclosed_name()` is used (zip crate's path sanitization), but if the zip entry name starts with `../` and `enclosed_name()` returns `None`, the entry is skipped with `continue`. This mitigates zip slip for standard zip entries — but other path normalization issues could still exist. | ||
| 145 | |||
| 146 | ### CFD Slice 1: Auth Middleware Chain | ||
| 147 | |||
| 148 | ``` | ||
| 149 | Request ──▶ Router | ||
| 150 | │ | ||
| 151 | ├── GET /login ──▶ login_page() [NO AUTH] | ||
| 152 | ├── POST /login ──▶ authenticate() [NO AUTH] | ||
| 153 | ├── POST /logout ──▶ logout() [NO AUTH middleware — relies on cookie parsing] | ||
| 154 | │ | ||
| 155 | ├── /static/* ──▶ ServeDir [NO AUTH] | ||
| 156 | │ | ||
| 157 | └── Protected Router ──▶ require_auth middleware | ||
| 158 | │ │ | ||
| 159 | │ ┌──────▼──────┐ | ||
| 160 | │ │ Parse Cookie │ | ||
| 161 | │ │ header │ | ||
| 162 | │ │ mcui_session │ | ||
| 163 | │ └──────┬───────┘ | ||
| 164 | │ │ | ||
| 165 | │ ┌──────▼───────┐ | ||
| 166 | │ │ Lookup in │ | ||
| 167 | │ │ sessions │ | ||
| 168 | │ │ table │ | ||
| 169 | │ └──────┬───────┘ | ||
| 170 | │ Found? │ Not found? | ||
| 171 | │ ┌──▼──┐ ┌──▼───┐ | ||
| 172 | │ │ OK │ │Redirect│ | ||
| 173 | │ │next()│ │/login │ | ||
| 174 | │ └─────┘ └───────┘ | ||
| 175 | │ | ||
| 176 | ├── / → dashboard | ||
| 177 | ├── /instances/* → instance CRUD | ||
| 178 | ├── /players/* → whitelist/op/ban | ||
| 179 | ├── /accounts/* → account management [superuser only] | ||
| 180 | ├── /backups/* → backup operations | ||
| 181 | └── /change-password → password change | ||
| 182 | ``` | ||
| 183 | |||
| 184 | --- | ||
| 185 | |||
| 186 | ## Attack Surface | ||
| 187 | |||
| 188 | ### Attacker-Controlled Input Sources | ||
| 189 | |||
| 190 | | Source | Entry point | Max size / rate | Risk | | ||
| 191 | |--------|------------|-----------------|------| | ||
| 192 | | `POST /login` form | `username`, `password` strings | ~4KB per field | Low — auth only | | ||
| 193 | | `POST /instances/new` form | `name`, `version`, `seed`, `motd` etc. | Form fields | Medium — SQLite INSERT | | ||
| 194 | | `POST /instances/{id}/edit` form | `difficulty`, `gamemode`, `motd` etc. | Form fields | Medium — SQLite UPDATE | | ||
| 195 | | `POST /instances/{id}/mods` multipart | `.jar`/`.zip` file bytes, filename | 500MB per field | **High** — file write, class parsing | | ||
| 196 | | `POST /instances/{id}/world/import` multipart | `.zip` file bytes | Multipart-limited | **High** — zip extraction | | ||
| 197 | | `POST /players/*` forms | usernames via RCON | ~16 chars | Medium — RCON injection | | ||
| 198 | | `GET /instances/{id}/world/export` | instance ID from path | N/A | Medium — file read | | ||
| 199 | | `GET /backups/{id}/download` | backup ID from path | N/A | Medium — file read | | ||
| 200 | | `POST /accounts/create` | `username`, `password` | form fields | Medium — superuser-only | | ||
| 201 | |||
| 202 | ### Execution Environments | ||
| 203 | |||
| 204 | 1. **WebUI Rust process** — local user, Docker socket access, SQLite access, filesystem write | ||
| 205 | 2. **Docker container** — itzg/minecraft-server, Java process, local network, RCON port | ||
| 206 | 3. **Outbound HTTP** — reqwest to Mojang API (no user-controlled URL at present) | ||
| 207 | |||
| 208 | --- | ||
| 209 | |||
| 210 | ## Key Dependencies | ||
| 211 | |||
| 212 | | Dependency | Version | Security role | Known CVEs | Reachability | | ||
| 213 | |-----------|---------|---------------|------------|-------------| | ||
| 214 | | **axum** | 0.8.9 | HTTP framework, routing, extractors | None recent | Every handler | | ||
| 215 | | **argon2** | 0.5.3 | Password hashing (Argon2id) | None — well-vetted | Auth flow only | | ||
| 216 | | **rusqlite** | 0.40.1 (bundled) | SQLite — all persistent data | SQLite bundled, no network | Every DB operation | | ||
| 217 | | **bollard** | 0.17.1 | Docker Engine API | Low (Docker socket) | Container ops | | ||
| 218 | | **zip** | 8.6.0 | ZIP archive parsing/writing | Zip slip in some versions — 8.6.0 has `enclosed_name()` | Mod upload, world import, backups | | ||
| 219 | | **multer** | 3.1.0 | Multipart/form-data parser | None recent | Mod upload only | | ||
| 220 | | **reqwest** | 0.13.4 | HTTP client to Mojang API | None recent | Version manifest fetch | | ||
| 221 | | **cookie** | 0.18.1 | Cookie parsing | None recent | Auth middleware | | ||
| 222 | | **axum-extra** | 0.12.6 | Cookie private jar | Not used for signing — feature listed but unused in code | — | | ||
| 223 | | **tokio** | 1.52.3 | Async runtime | None recent | Every async operation | | ||
| 224 | | **rand** | 0.8.7 | RCON password generation | Std RNG, not crypto-grade | RCON password | | ||
| 225 | | **tower-http** | 0.7.0 | Static file serving | None recent | `/static/*` | | ||
| 226 | |||
| 227 | **Security-relevant observations:** | ||
| 228 | - `rand::thread_rng()` (not `OsRng`) used for RCON password generation — not cryptographic-grade | ||
| 229 | - `axum-extra` `cookie-private` feature listed but unused — potential dead feature | ||
| 230 | - `zip` 8.6.0 uses `enclosed_name()` which requires explicit check — it IS checked in world import | ||
| 231 | |||
| 232 | --- | ||
| 233 | |||
| 234 | ## Framework Contracts and Hidden Control Channels | ||
| 235 | |||
| 236 | ### Axum Framework Contracts | ||
| 237 | |||
| 238 | 1. **Request path extraction** — `Path<i64>` extractors in handlers for instance/backup IDs: | ||
| 239 | - Axum validates that the path segment is a valid integer | ||
| 240 | - However, ID is used directly in SQL queries (no injection due to type safety of rusqlite params) | ||
| 241 | |||
| 242 | 2. **Form extraction** — `axum::Form<T>` deserializes `application/x-www-form-urlencoded` bodies: | ||
| 243 | - Strings accepted as-is, passed to SQL INSERT/UPDATE via `params![]` macro | ||
| 244 | - SQL injection prevented by rusqlite's parameterized queries | ||
| 245 | |||
| 246 | 3. **Multipart extraction** — manual multer parsing (not axum's built-in `Multipart`): | ||
| 247 | - Boundary parsed from Content-Type header; `Constraints` limit 500MB | ||
| 248 | - Filename from `field.file_name()` — **no sanitization** beyond stripping directory with `Path::new(filename).file_name()` | ||
| 249 | |||
| 250 | 4. **Extension-based auth** — `require_auth` places `Account` in `request.extensions_mut()`: | ||
| 251 | - All protected handlers get `CurrentUser` via `FromRequestParts` | ||
| 252 | - This is a framework-level contract — extensions survive across middleware boundaries | ||
| 253 | |||
| 254 | ### Cookie/Hidden Channel Contracts | ||
| 255 | |||
| 256 | | Header/Cookie | Set by | Read by | Security property | | ||
| 257 | |--------------|--------|---------|-------------------| | ||
| 258 | | `Cookie: mcui_session` | `authenticate()` in auth.rs | `require_auth()` middleware | Session identifier | | ||
| 259 | | `Content-Type` | Client | Multer parser in mods.rs | Boundary extraction for multipart | | ||
| 260 | | User-supplied filename | Client | `field.file_name()` in mods.rs | Written to disk directly | | ||
| 261 | |||
| 262 | ### Proxy Assumptions | ||
| 263 | |||
| 264 | - Server binds to `127.0.0.1:<port>` (localhost-only) | ||
| 265 | - **No TLS** — assumes a reverse proxy (nginx) handles HTTPS termination | ||
| 266 | - `X-Forwarded-*` headers are **not read** anywhere — trust decisions based on direct connection | ||
| 267 | - No CDN, no WAF, no rate limiting in the application | ||
| 268 | |||
| 269 | ### Auth Guard Analysis | ||
| 270 | |||
| 271 | - **`require_auth` middleware** (src/auth.rs:55): applied to the `protected` router group | ||
| 272 | - **Superuser checks**: done **inside handlers** (e.g., `if !account.is_superuser { return Err(Redirect::to("/")); }`) | ||
| 273 | - **No re-check** on sub-routes within the protected group — any authenticated user can access all instance/player/backup operations | ||
| 274 | - **Logout handler** (`POST /logout`): **NOT behind `require_auth`** — it's registered on the outer Router outside the `protected` group. This means anonymous requests could cause session deletion if they can guess a valid session ID from the cookie header. (Low risk due to session ID being UUID v4 — practically unpredictable.) | ||
| 275 | |||
| 276 | --- | ||
| 277 | |||
| 278 | ## Threat Model | ||
| 279 | |||
| 280 | ### Assets | ||
| 281 | |||
| 282 | | Asset | Description | Storage | Impact if compromised | | ||
| 283 | |-------|------------|---------|-----------------------| | ||
| 284 | | **Minecraft server instance** | Running Docker container with world, mods, config | Docker + filesystem | Game data loss, server hijack | | ||
| 285 | | **World data** | Minecraft saves (chunks, player inventories) | Filesystem (ZIP backups too) | Permanent data loss | | ||
| 286 | | **Accounts** | WebUI credentials and sessions | SQLite (Argon2id hashed) | Unauthorized web access | | ||
| 287 | | **RCON password** | Auto-generated, per-instance | SQLite (plaintext) + server.properties | Minecraft server control | | ||
| 288 | | **Mod files** | Uploaded JARs | Filesystem | Supply chain risk (arbitrary code in JVM) | | ||
| 289 | | **Docker socket** | `/var/run/docker.sock` | Host filesystem | Container escape, host compromise | | ||
| 290 | | **Config** | Server directory, initial superuser creds | config.toml | Full system access | | ||
| 291 | |||
| 292 | ### Threat Actors | ||
| 293 | |||
| 294 | | Actor | Capability | Motivation | | ||
| 295 | |-------|-----------|------------| | ||
| 296 | | **Anonymous web attacker** | Can reach localhost:3000 if exposed | Access webUI, break auth, RCE | | ||
| 297 | | **Authenticated user** | Valid session; equal access to all instances | Escalate to superuser, exfiltrate data | | ||
| 298 | | **Superuser** | Full webUI control | Any action webUI authorizes | | ||
| 299 | | **Network-level attacker** | Can reach Docker API, RCON port, Minecraft port | Side-channel or direct attack | | ||
| 300 | | **Supply-chain attacker** | Malicious mod JAR | Server-side code execution in Minecraft JVM | | ||
| 301 | |||
| 302 | ### Attack Scenarios (Prioritized) | ||
| 303 | |||
| 304 | | # | Threat | Actor | Vector | Impact | Existing Control | Priority | | ||
| 305 | |---|--------|-------|--------|--------|-----------------|----------| | ||
| 306 | | T1 | **World import zip slip** | Auth user | ZIP with `../` entries bypasses `enclosed_name()` → write files outside data dir | Filesystem write anywhere webUI user can write | `enclosed_name()` check in world import | **Medium** (mitigated for standard zip slip) | | ||
| 307 | | T2 | **Path traversal in mod filename** | Auth user | Upload mod with name `../../cronjob` → write to parent dirs | Arbitrary file write | `file_name()` call strips directory components | **Medium** (partially mitigated) | | ||
| 308 | | T3 | **Backup path traversal (download)** | Auth user | Backup filename with path traversal chars in DB | File read outside backup dir | Filename read from DB (stored at insert time) | **Low** | | ||
| 309 | | T4 | **Session fixation/logout bypass** | Anonymous | POST /logout can delete sessions without auth middleware | Force logout of legitimate user | UUIDv4 session IDs are unpredictable | **Low** | | ||
| 310 | | T5 | **RCON password plaintext in DB** | Auth user/Superuser | Read rcon_password from instances table via edit form | Minecraft server command injection | RCON password stored in DB, returned in edit form | **Medium** | | ||
| 311 | | T6 | **Weak RCON password generation** | Network attacker | `rand::thread_rng()` used for 16-char alphanumeric password | RCON brute force | 16 chars alphanumeric = 36^16 space | **Low** | | ||
| 312 | | T7 | **No rate limiting on login** | Anonymous | Brute-force login credentials | Account compromise | No rate limiting implemented | **High** | | ||
| 313 | | T8 | **No CSRF protection** | Any | Cross-site request forgery on any POST endpoint | State-changing operations without user intent | None — no CSRF token anywhere | **High** | | ||
| 314 | | T9 | **Mod JAR parsing denial of service** | Auth user | Upload crafted JAR that causes OOM or hangs in class-file scanner (constant pool loops) | Server DoS | No timeout on validate_mod() | **Medium** | | ||
| 315 | | T10 | **Large file upload DoS** | Auth user | 500MB max per field, but no per-request total limit | Memory exhaustion | 500MB per-field limit, but multipart streams | **Medium** | | ||
| 316 | |||
| 317 | --- | ||
| 318 | |||
| 319 | --- | ||
| 320 | |||
| 321 | ## Static Analysis Summary | ||
| 322 | |||
| 323 | **Phase:** P4 (Static Analysis & Triage) | ||
| 324 | **Date:** 2025-07-29 | ||
| 325 | **Mode:** deep (grep+read fallback — codeql and semgrep CLIs not available on this system) | ||
| 326 | |||
| 327 | ### Sub-step 4.1 — Structural Extraction Results | ||
| 328 | |||
| 329 | | Metric | Value | | ||
| 330 | |--------|-------| | ||
| 331 | | Entry points (HTTP handlers) | 31 (including static file serve) | | ||
| 332 | | Sinks identified | 14 unique sink types | | ||
| 333 | | Source-to-sink slices | 12 documented flows | | ||
| 334 | | Files analyzed | 15 Rust source files | | ||
| 335 | | Third-party JS minified file candidates | 26 matches (all in htmx.min.js — all false positives) | | ||
| 336 | |||
| 337 | ### Tools Used | ||
| 338 | |||
| 339 | - **grep + read** — manual structural extraction and flow tracing (codeql CLI not installed) | ||
| 340 | - **Manual source-to-sink analysis** — all 12 critical/high/medium flows traced | ||
| 341 | - **Custom rules** — 4 CodeQL query stubs + 15 Semgrep rules created for reference | ||
| 342 | - **Built-in suites:** Not applicable — codeql/semgrep CLIs not available | ||
| 343 | |||
| 344 | ### Custom SAST Artifacts | ||
| 345 | |||
| 346 | | Artifact | Location | Description | | ||
| 347 | |----------|----------|-------------| | ||
| 348 | | Entry points | `piolium/codeql-artifacts/entry-points.json` | 31 HTTP entry points catalogued | | ||
| 349 | | Sinks | `piolium/codeql-artifacts/sinks.json` | 14 sink types with locations | | ||
| 350 | | Call graph slices | `piolium/codeql-artifacts/call-graph-slices.json` | 12 source-to-sink data flow slices | | ||
| 351 | | Source-sink flows | `piolium/attack-surface/source-sink-flows-all-severities.md` | Full flow paths by severity | | ||
| 352 | | Custom CodeQL queries | `piolium/codeql-queries/` | 4 query stubs (path-traversal, rcon-injection, csrf, session-ttl) | | ||
| 353 | | Custom Semgrep rules | `piolium/semgrep-rules/security-rules.yml` | 15 detection rules | | ||
| 354 | | Draft findings | `piolium/findings-draft/` | 30 draft findings (p4-001 through p4-030) | | ||
| 355 | |||
| 356 | ### DFD/CFD Slices Driving Custom Analysis | ||
| 357 | |||
| 358 | Based on Phase 3 DFD/CFD analysis, custom targeted analysis was performed for: | ||
| 359 | |||
| 360 | | Slice | Custom Rule Focus | Finding | | ||
| 361 | |-------|-------------------|---------| | ||
| 362 | | DFD Slice 2 (Mod Upload) | Mod filename path traversal, mod validation DoS, ZIP recursion | p4-010, p4-014, p4-015 | | ||
| 363 | | DFD Slice 3 (World Import) | ZIP slip edge cases | p4-011 | | ||
| 364 | | CFD Slice 1 (Auth Middleware) | No session TTL, no Secure flag, cookie parsing | p4-004, p4-009, p4-019 | | ||
| 365 | | Hidden Control Channels | Cookie auth, Content-Type boundary parsing | p4-008, p4-019 | | ||
| 366 | | RCON Player Actions | RCON command injection | p4-002 | | ||
| 367 | | Instance Creation | Instance name path traversal, input validation | p4-001, p4-018 | | ||
| 368 | |||
| 369 | ### Inline Enrichment Summary | ||
| 370 | |||
| 371 | See the `## SAST Enrichment` section below for per-candidate classification and keep/drop decisions. | ||
| 372 | |||
| 373 | --- | ||
| 374 | |||
| 375 | ## CodeQL Structural Analysis | ||
| 376 | |||
| 377 | CodeQL was not available on this system. Structural extraction was performed manually using grep + read. | ||
| 378 | |||
| 379 | **Database:** Not built (codeql CLI not installed). Artifacts in `piolium/codeql-artifacts/` are manually constructed. | ||
| 380 | |||
| 381 | **Entry points found:** 31 (all HTTP route handlers) — see `piolium/codeql-artifacts/entry-points.json` | ||
| 382 | |||
| 383 | **Sinks found:** 14 categories — see `piolium/codeql-artifacts/sinks.json` | ||
| 384 | |||
| 385 | **Source-to-sink traces:** 12 documented flows — see `piolium/codeql-artifacts/call-graph-slices.json` and `piolium/attack-surface/source-sink-flows-all-severities.md` | ||
| 386 | |||
| 387 | Key flows by severity: | ||
| 388 | - **Critical:** Instance name path traversal (flow-1) | ||
| 389 | - **High:** RCON command injection (flow-4, flow-5), No session TTL (hidden control channel), No CSRF | ||
| 390 | - **Medium:** Mod filename special chars (flow-3), World import ZIP (flow-6), Backup path (flow-7), Docker env injection (flow-11), Seed injection (flow-12) | ||
| 391 | - **Low:** Content-type parsing, Static file serving, Cookie parsing, Template errors | ||
| 392 | |||
| 393 | --- | ||
| 394 | |||
| 395 | ## SAST Enrichment | ||
| 396 | |||
| 397 | ### Candidate Classification | ||
| 398 | |||
| 399 | All 26 candidate matches from the candidate scan were in `static/htmx.min.js` — a minified third-party JavaScript library (htmx v2.x). All candidates are **false positives** from the minified file. | ||
| 400 | |||
| 401 | | Finding | Classification | Attacker Control | Boundary | CodeQL Reachability | Verdict | | ||
| 402 | |---------|---------------|-----------------|----------|-------------------|---------| | ||
| 403 | | dynamic-code-execution (x8, htmx.min.js) | env/tooling-only | N/A (minified 3rd-party lib) | N/A | no-slice | **DROP** — minified 3rd-party lib, not project code | | ||
| 404 | | command-execution (x4, htmx.min.js) | env/tooling-only | N/A (minified 3rd-party lib) | N/A | no-slice | **DROP** — minified 3rd-party lib, not project code | | ||
| 405 | | path-traversal-file-access (x7, htmx.min.js) | env/tooling-only | N/A (minified 3rd-party lib) | N/A | no-slice | **DROP** — minified 3rd-party lib, not project code | | ||
| 406 | | unsafe-html-or-template (x1, htmx.min.js) | env/tooling-only | N/A (minified 3rd-party lib) | N/A | no-slice | **DROP** — minified 3rd-party lib, not project code | | ||
| 407 | | hidden-control-channel (x6, htmx.min.js) | env/tooling-only | N/A (minified 3rd-party lib) | N/A | no-slice | **DROP** — minified 3rd-party lib, not project code | | ||
| 408 | |||
| 409 | ### True Findings Identified Through Manual Analysis | ||
| 410 | |||
| 411 | All true findings were identified through manual structural analysis of the Rust source code. See `piolium/findings-draft/` for 30 drafted findings. | ||
| 412 | |||
| 413 | ### Entry Points Not in Phase 3 DFD Slices | ||
| 414 | |||
| 415 | All DFD/CFD entry points from Phase 3 were covered. Additional unauthenticated paths not explicitly listed in Phase 3 DFD slices: | ||
| 416 | - `GET /static/*` — static file serving (no auth) | ||
| 417 | - `POST /logout` — logout without auth middleware | ||
| 418 | |||
| 419 | ### Unmodeled High-Risk Flows | ||
| 420 | |||
| 421 | - **Docker environment variable injection** (flow-11) — user-controlled instance settings pass into Docker container env without sanitization | ||
| 422 | - **Level-seed injection into server.properties** (flow-12) — seed written to config file without escaping | ||
| 423 | |||
| 424 | --- | ||
| 425 | |||
| 426 | ## Domain Attack Research | ||
| 427 | |||
| 428 | ### Domain: ZIP Archive Handling | ||
| 429 | |||
| 430 | **Identified via:** `zip` crate dependency (8.6.0), world import (ZIP extraction), mod upload (nested JAR-in-ZIP), backup creation/restore | ||
| 431 | |||
| 432 | **Known attack classes:** | ||
| 433 | |||
| 434 | | Attack | Description | Detection strategy | Relevance | | ||
| 435 | |--------|-------------|-------------------|-----------| | ||
| 436 | | **Zip Slip** | Archive entry name with `../` writes outside target dir | Check all `file.enclosed_name()` calls + fallback paths | High — world import and mod ZIP both extract entries | | ||
| 437 | | **Zip Bomb** | Tiny archive expands to gigabytes; nested zip | Size limits, streaming extraction, recursion depth | Medium — 500MB stream limit partially mitigates | | ||
| 438 | | **Uncontrolled recursion depth** | ZIP within ZIP within ZIP | Recursion depth tracking in process_zip_upload | High — recursion via process_jar_upload→validate_mod has no depth limit | | ||
| 439 | | **Symlink traversal** | ZIP entry creates symlink outside target dir | Check zip crate version for symlink handling | Low — zip crate doesn't create symlinks by default | | ||
| 440 | | **Empty filename / directory traversal via encoding** | Different OS path normalization of entry names | Test with `\` vs `/` on different platforms | Low — Rust std::fs on Linux doesn't do special normalization | | ||
| 441 | |||
| 442 | **Custom SAST targets:** | ||
| 443 | |||
| 444 | | Attack pattern | Rule type | Source/sink or pattern | Priority | | ||
| 445 | |---------------|-----------|----------------------|----------| | ||
| 446 | | Zip entry path not using `enclosed_name()` | Semgrep | `archive.by_index(i)` → `.name()` but no `enclosed_name()` | High | | ||
| 447 | | Zip extraction without size limit | Semgrep | `archive.by_index(i)` → `read_to_end()` without size check | Medium | | ||
| 448 | | Recursive zip extraction no depth limit | CodeQL | Recursive call to extract function without depth counter | High | | ||
| 449 | |||
| 450 | **Manual review checklist:** | ||
| 451 | - [ ] All ZIP extraction uses `enclosed_name()` — checked in world import (src/routes/instances.rs:import_world) | ||
| 452 | - [ ] Mod ZIP extraction (src/routes/mods.rs:process_zip_upload) — only extracts `.jar` entries, reads them into memory | ||
| 453 | - [ ] No depth limit on nested ZIPs inside mod upload (process_zip_upload → process_jar_upload → validate_mod — validate_mod opens the JAR as a ZIP) | ||
| 454 | |||
| 455 | ### Domain: File Upload (Mod Upload) | ||
| 456 | |||
| 457 | **Identified via:** multer dependency, multipart handler for `.jar`/`.zip` files | ||
| 458 | |||
| 459 | **Known attack classes:** | ||
| 460 | |||
| 461 | | Attack | Description | Detection strategy | Relevance | | ||
| 462 | |--------|-------------|-------------------|-----------| | ||
| 463 | | **Unrestricted file upload** | Any file extension accepted | Check file extension filtering — `.jar`/`.zip` only | High — only JAR/ZIP accepted | | ||
| 464 | | **Path traversal via filename** | `../../evil.so` as upload filename | Filename sanitized via `file_name()` | High — partially sanitized | | ||
| 465 | | **Large file DoS** | 500MB upload exhausts memory | Multer size limit configured at 500MB | Medium — 500MB is high | | ||
| 466 | | **Content-type not validated** | No MIME type check | No content-type validation on upload | Medium — relies on extension | | ||
| 467 | |||
| 468 | **Manual review checklist:** | ||
| 469 | - [ ] Filename sanitized with `Path::new(filename).file_name()` in process_jar_upload — YES, directory components stripped | ||
| 470 | - [ ] File extension validated before processing — YES, `.jar`/`.zip`/`.JAR`/`.ZIP` only | ||
| 471 | - [ ] Uploaded file size validated — YES, 500MB per-field limit via multer Constraints | ||
| 472 | - [ ] File written before or after validation — AFTER validation (accept/reject) | ||
| 473 | - [ ] Rejected files are NOT written to disk — Correct; only `result.accepted` files are written | ||
| 474 | |||
| 475 | ### Domain: Session Management | ||
| 476 | |||
| 477 | **Identified via:** Custom cookie-based sessions, UUID v4 session IDs | ||
| 478 | |||
| 479 | **Known attack classes:** | ||
| 480 | |||
| 481 | | Attack | Description | Detection strategy | Relevance | | ||
| 482 | |--------|-------------|-------------------|-----------| | ||
| 483 | | **Session fixation** | Attacker sets known session ID before login | Session ID regenerated on login (new UUID issued) | Low — new UUID per login | | ||
| 484 | | **Session not invalidated on logout** | Server-side session persists | Logout handler deletes session from DB | High — logout DOES delete from DB | | ||
| 485 | | **No absolute session timeout** | Sessions valid indefinitely | No `expires_at` or `max-age` check on server side | **High** — sessions stored without TTL | | ||
| 486 | | **Missing Secure flag** | Cookie sent over HTTP | Cookie set with HttpOnly but NOT Secure | Medium — behind nginx but not marked | | ||
| 487 | | **CSRF via session cookie** | Cookie sent automatically | No SameSite=Strict (only SameSite=Lax) | Medium — POST operations still protected by Lax for same-site | | ||
| 488 | | **Brute-force session ID** | Predictable session IDs | UUIDv4 is random | Low — 122 bits of entropy | | ||
| 489 | |||
| 490 | **Manual review checklist:** | ||
| 491 | - [ ] Session ID regenerated on login — YES (UUIDv4 on each login) | ||
| 492 | - [ ] Server-side session deletion on logout — YES (DELETE FROM sessions WHERE id = ?1) | ||
| 493 | - [ ] Absolute timeout enforced — **NO** — no `expires_at` column or check | ||
| 494 | - [ ] Cookie flags: HttpOnly=YES, Secure=NO, SameSite=Lax=YES | ||
| 495 | - [ ] Max-age=86400 (24h) but no server-side enforcement after that | ||
| 496 | |||
| 497 | ### Domain: RCON Protocol | ||
| 498 | |||
| 499 | **Identified via:** Custom TCP client to 127.0.0.1:25575 | ||
| 500 | |||
| 501 | **Known attack classes:** | ||
| 502 | |||
| 503 | | Attack | Description | Detection strategy | Relevance | | ||
| 504 | |--------|-------------|-------------------|-----------| | ||
| 505 | | **Command injection via username** | Unsanitized username sent in RCON command | Username from form → `format!("whitelist add {}", form.username)` | **Medium** — username contains no shell chars but could inject RCON commands via spaces/semicolons | | ||
| 506 | | **RCON password exposure** | Plaintext password in DB and edit form | Stored as `rcon_password` in instances table | **High** — any authenticated user can see it | | ||
| 507 | | **RCON man-in-the-middle** | No TLS on RCON connection | Localhost only, plain TCP | Low — localhost only | | ||
| 508 | | **Authentication replay** | RCON auth packet capture | No session binding | Low — per-instance random password | | ||
| 509 | |||
| 510 | **Manual review checklist:** | ||
| 511 | - [ ] Username validated for RCON safety — NO — `format!("whitelist add {}", form.username)` — no sanitization on username | ||
| 512 | - [ ] RCON password generation uses crypto RNG — **NO** — `rand::thread_rng()` used (not `OsRng`) | ||
| 513 | - [ ] RCON password accessible to users — **YES** — returned in edit form via `rcon_password` field | ||
| 514 | |||
| 515 | ### Domain: ZIP Slip — Mod Nested ZIP Extraction | ||
| 516 | |||
| 517 | **Identified via:** process_zip_upload in src/routes/mods.rs extracts JARs from user-supplied ZIPs | ||
| 518 | |||
| 519 | **Attack class:** Recursive extraction with no depth limit — a ZIP containing a JAR (which is also a ZIP) is opened by `validate_mod` which calls `ZipArchive::new` on the extracted bytes. | ||
| 520 | |||
| 521 | **Manual review checklist:** | ||
| 522 | - [ ] Recursive extraction depth limited — **NO** — no depth counter in process_zip_upload → process_jar_upload → validate_mod | ||
| 523 | - [ ] Size of extracted JAR bytes bounded before passing to validate_mod — bounded by multer, but nested extraction may allocate | ||
| 524 | |||
| 525 | --- | ||
| 526 | |||
| 527 | ## Phase 4 CodeQL Extraction Targets | ||
| 528 | |||
| 529 | | DFD Slice | Source type | Sink kind | File:line | Rationale | | ||
| 530 | |-----------|------------|-----------|-----------|-----------| | ||
| 531 | | Mod upload file write | RemoteFlowSource | file-write | src/routes/mods.rs:98 (`std::fs::write`) | Attacker-controlled filename → arbitrary file write | | ||
| 532 | | World import ZIP extraction | RemoteFlowSource | file-write | src/routes/instances.rs:210 (`std::fs::File::create`) | ZIP entry path → file creation | | ||
| 533 | | Backup download path | RemoteFlowSource | file-read | src/routes/backups.rs:106 (`std::fs::read`) | Backup ID from DB → file read | | ||
| 534 | | Login username | RemoteFlowSource | sql-execution | src/auth.rs:45 (`query_row`) | String from form → SQL query (parametrized) | | ||
| 535 | | Create instance name | RemoteFlowSource | sql-execution | src/routes/instances.rs:119 (`execute`) | Name from form → SQL INSERT | | ||
| 536 | | Mod upload filename | RemoteFlowSource | path-traversal | src/routes/mods.rs:187 (`mods_dir.join(&safe_name)`) | Stripped but could still contain special chars | | ||
| 537 | | Player action username | RemoteFlowSource | command-execution | src/routes/players.rs:94 (`format!($command, form.username)`) | Username → RCON command string | | ||
| 538 | |||
| 539 | --- | ||
| 540 | |||
| 541 | ## Spec Gap Candidates | ||
| 542 | |||
| 543 | No formal specs or RFCs are implemented by this project. The application implements: | ||
| 544 | 1. **RCON protocol** (Minecraft Remote Console) — ad hoc binary protocol, not formally specified, but follows Minecraft community convention | ||
| 545 | 2. **Argon2id password hashing** (via `argon2` crate) — follows RFC 9106 | ||
| 546 | 3. **UUID v4** (via `uuid` crate) — follows RFC 4122 | ||
| 547 | 4. **`.properties` file parsing** — Java convention, ad hoc implementation in src/properties.rs | ||
| 548 | |||
| 549 | **Gap:** | ||
| 550 | - The RCON implementation (src/rcon.rs) follows Minecraft protocol but has no nonce or bound authentication — classic RCON vulnerability where any authenticated session can issue any command | ||
| 551 | - No explicit rate limiting spec — none implemented | ||
| 552 | - No session time-out spec — none enforced server-side | ||
| 553 | |||
| 554 | --- | ||
| 555 | |||
| 556 | ## Known False-Positive Sources | ||
| 557 | |||
| 558 | None identified — INFO.md was not present. | ||
| 559 | |||
| 560 | --- | ||
| 561 | |||
| 562 | ## Out-of-Scope Paths | ||
| 563 | |||
| 564 | - `minecraft/` — contains actual runtime data for a Minecraft server instance (world, logs, mods, configs). These are generated runtime artifacts, not source code. | ||
| 565 | - `target/` — Rust build artifacts | ||
| 566 | - `.git/` — VCS data | ||
| 567 | - `piolium/` — audit artifacts | ||
| 568 | |||
| 569 | --- | ||
| 570 | |||
| 571 | ## Coverage Gaps | ||
| 572 | |||
| 573 | 1. **No CSRF protection** — all POST endpoints are vulnerable to cross-site request forgery | ||
| 574 | 2. **No rate limiting** — login endpoint, mod upload, any POST form | ||
| 575 | 3. **No session TTL enforcement** — server never checks `created_at` on sessions | ||
| 576 | 4. **No audit logging** — no record of who performed what action (except RCON commands logged via tracing) | ||
| 577 | 5. **No input validation for RCON username** — could contain spaces or special RCON-parsed characters | ||
| 578 | 6. **No backup restore implementation** — `restore_backup` handler returns a redirect without performing any restore | ||
| 579 | 7. **No encryption at rest** — SQLite DB unencrypted on disk | ||
| 580 | 8. **No API key or token auth** — only cookie-based session auth | ||
| 581 | |||
| 582 | ## State & Concurrency Audit | ||
| 583 | |||
| 584 | - State-holding entities catalogued: 10 (across 5 tables: accounts, sessions, instances, mods, backups) | ||
| 585 | - Concurrency primitives observed: `Arc<Mutex<Connection>>` (per-statement serialisation, no transaction boundaries), SQLite WAL mode | ||
| 586 | - Idempotency infrastructure: **absent** — no idempotency keys, no dedup, no request ID storage | ||
| 587 | - Drafts filed: 6 (split by class: 3 TOCTOU, 1 state-machine-violation, 1 idempotency, 1 read-modify-write) | ||
| 588 | - See `piolium/attack-surface/state-concurrency-summary.md` for full catalogue | ||
| 589 | |||
| 590 | ## Cross-Service Taint Propagation | ||
| 591 | |||
| 592 | - Services analysed: 1 (single-process Rust/axum server — `src/main.rs` only) | ||
| 593 | - Edges stitched: 0 — no inter-service edges detected | ||
| 594 | - Coverage gaps: N/A | ||
| 595 | - Drafts filed: 0 | ||
| 596 | |||
| 597 | Skipped — single-service project; the only data flows cross boundaries to external infrastructure (Docker daemon via Unix socket, Minecraft server via RCON TCP, SQLite file, filesystem) which are not deployable services within this repo. | ||
