summaryrefslogtreecommitdiffstats
path: root/piolium/attack-surface/knowledge-base-report.md
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--piolium/attack-surface/knowledge-base-report.md597
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.
22Binds 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
461. **HTTP** (localhost:3000) — all web traffic
472. **Docker Unix Socket** (`/var/run/docker.sock`) — container management
483. **TCP** (localhost:25575) — RCON protocol to Minecraft container
494. **TCP** (0.0.0.0:25565) — Minecraft client connections (managed by container, not webUI)
505. **HTTPS** (outbound to Mojang API) — version manifest fetch
51
52### Trust Boundaries
53
54```
55HTTP 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:**
691. **Public→Protected routes:** session cookie check via `require_auth` middleware (src/auth.rs:55)
702. **WebUI→Docker:** local Unix socket — any code running as the webUI user can control containers
713. **WebUI→RCON:** localhost TCP — password-protected, but breakable if rcon_password exposed
724. **WebUI→Database:** same-process SQLite — no isolation between auth and data ops
735. **Mod upload→Filesystem:** validated JAR → instance mods directory (path traversal risk)
746. **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```
128Attacker ──▶ 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```
149Request ──▶ 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
2041. **WebUI Rust process** — local user, Docker socket access, SQLite access, filesystem write
2052. **Docker container** — itzg/minecraft-server, Java process, local network, RCON port
2063. **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
2381. **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
2422. **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
2463. **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
2504. **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
358Based 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
371See the `## SAST Enrichment` section below for per-candidate classification and keep/drop decisions.
372
373---
374
375## CodeQL Structural Analysis
376
377CodeQL 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
387Key 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
399All 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
411All 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
415All 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
543No formal specs or RFCs are implemented by this project. The application implements:
5441. **RCON protocol** (Minecraft Remote Console) — ad hoc binary protocol, not formally specified, but follows Minecraft community convention
5452. **Argon2id password hashing** (via `argon2` crate) — follows RFC 9106
5463. **UUID v4** (via `uuid` crate) — follows RFC 4122
5474. **`.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
558None 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
5731. **No CSRF protection** — all POST endpoints are vulnerable to cross-site request forgery
5742. **No rate limiting** — login endpoint, mod upload, any POST form
5753. **No session TTL enforcement** — server never checks `created_at` on sessions
5764. **No audit logging** — no record of who performed what action (except RCON commands logged via tracing)
5775. **No input validation for RCON username** — could contain spaces or special RCON-parsed characters
5786. **No backup restore implementation** — `restore_backup` handler returns a redirect without performing any restore
5797. **No encryption at rest** — SQLite DB unencrypted on disk
5808. **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
597Skipped — 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.