diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-29 02:24:41 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-29 02:24:41 +0200 |
| commit | c654a6d17eb58adbb15b9c47ab32bcc3a1666307 (patch) | |
| tree | 545544bf7d805254415da787fad1cdc711767833 /piolium/attack-surface/unauthenticated-surface.md | |
| parent | Add per-instance mod management with client-side detection (diff) | |
| download | minecraft-ui-c654a6d17eb58adbb15b9c47ab32bcc3a1666307.tar.gz minecraft-ui-c654a6d17eb58adbb15b9c47ab32bcc3a1666307.zip | |
Security audit fixes: patch 16 vulnerabilities across 7 source files
CRITICAL:
- Instance name path traversal (AP-001): sanitize::sanitize_instance_name()
strips .., /, \, \0, spaces, special chars at all 12 join call sites
- Zip-slip protection in world import: starts_with() guard after enclosed_name()
HIGH:
- RCON command injection: validate_username() rejects non-MC usernames
across all 6 player action macros (whitelist, op, ban)
- Level-seed properties injection: sanitize_seed() strips \n \r = : \0
- CSRF protection: SameSite=Lax -> SameSite=Strict on session cookie
MEDIUM:
- Session TTL: server-side 24h expiry via strftime check in require_auth
- Rate limiting: in-memory 10-ip/5min sliding window on login
- Background task stale is_active: all queries use concrete WHERE id=?1
- Mod validation DoS: 500MB cap, 10k class scan limit, 10k CP entry cap
LOW:
- Logout auth guard: moved into protected route group + CurrentUser param
- Mod orphan race: DB insert before file write
- Backup orphan race: DB insert before ZIP creation
- Template fix: broken Askama == comparisons in edit.html
All fixes compile clean with zero warnings. 10 unit tests pass.
Diffstat (limited to '')
| -rw-r--r-- | piolium/attack-surface/unauthenticated-surface.md | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/piolium/attack-surface/unauthenticated-surface.md b/piolium/attack-surface/unauthenticated-surface.md new file mode 100644 index 0000000..d899854 --- /dev/null +++ b/piolium/attack-surface/unauthenticated-surface.md | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | # Unauthenticated Attack Surface | ||
| 2 | |||
| 3 | Reachable by an anonymous attacker — no valid session, token, or API key. | ||
| 4 | |||
| 5 | **Coverage**: 4 entry points | 3 by-design public | 1 missing-guard | 0 middleware-gap | ||
| 6 | **Auth model**: Cookie-based session (mcui_session UUIDv4) via `require_auth` middleware (src/auth.rs:55). Login sets Set-Cookie with HttpOnly; middleware extracts cookie header and queries sessions table. | ||
| 7 | **Coverage gaps**: None — all routes are statically registered in the axum Router at src/routes/mod.rs. No reflection-based or dynamically-registered handlers. | ||
| 8 | |||
| 9 | ## Pre-Auth HTTP / API Routes | ||
| 10 | |||
| 11 | | # | Method | Path | Handler (file:line) | Why pre-auth | Notable inputs / sinks | Blast radius | | ||
| 12 | |---|--------|------|---------------------|--------------|------------------------|--------------| | ||
| 13 | | 1 | GET | `/login` | `login_page` (src/routes/mod.rs:53) | by-design | None — renders static template | None — no side effects, no data access | | ||
| 14 | | 2 | POST | `/login` | `authenticate` (src/auth.rs:34) | by-design | `username`, `password` form fields → Argon2id password hash verification → session INSERT | Account compromise if credentials brute-forced (no rate limiting — see p4-005). Session fixation not possible (new UUID on each login). | | ||
| 15 | | 3 | POST | `/logout` | `logout` (src/auth.rs:130) | **missing-guard** (p4-008) | Cookie header → raw string split on `;` and `=` → session ID → `DELETE FROM sessions WHERE id = ?1` | Session deletion if valid session ID known. UUIDv4 is unpredictable (122 bits). Low practical risk but defense-in-depth violation. | | ||
| 16 | | 4 | GET | `/static/*` | `ServeDir::new("static")` (src/routes/mod.rs:28) | by-design | File path after `/static/` → static file read | Static file read only. Directory contains only `style.css` and `htmx.min.js`. No user data, no secrets, no configuration. | | ||
| 17 | |||
| 18 | ## Other Unauthenticated Entry Points | ||
| 19 | |||
| 20 | | Kind | Entry point (file:line) | Why pre-auth | Notes | | ||
| 21 | |------|-------------------------|--------------|-------| | ||
| 22 | | **Static file server** | `ServeDir::new("static")` mounted at Router level (src/routes/mod.rs:28) | by-design | Serves `static/` directory contents (style.css, htmx.min.js). No auth, no session check, no dynamic content. Served directly without any middleware. | | ||
| 23 | | **Logout session deletion** | `logout` handler (src/auth.rs:130) registered on **outer Router** (src/routes/mod.rs:20) | missing-guard (p4-008) | Covered by row #3 above. The handler is on the outer Router, NOT behind `require_auth`. Any anonymous request can reach it. The handler extracts session ID from Cookie header via raw string splitting (`split(';')` then `split_once('=')`) and attempts to delete the session from DB. | | ||
| 24 | |||
| 25 | ## Why `POST /logout` is classified `missing-guard` | ||
| 26 | |||
| 27 | The route registration at src/routes/mod.rs:20: | ||
| 28 | |||
| 29 | ```rust | ||
| 30 | Router::new() | ||
| 31 | .route("/login", axum::routing::get(login_page)) | ||
| 32 | .route("/login", axum::routing::post(crate::auth::authenticate)) | ||
| 33 | .route("/logout", axum::routing::post(crate::auth::logout)) // <-- OUTSIDE protected group | ||
| 34 | .merge(protected) // <-- protected has .layer(auth_required) | ||
| 35 | ``` | ||
| 36 | |||
| 37 | The `require_auth` middleware (src/auth.rs:55) is applied only to the `protected` router group. The logout handler processes the request without any session validation — it simply reads the Cookie header, extracts the session ID by splitting on `;` and `=`, and executes `DELETE FROM sessions WHERE id = ?1`. | ||
| 38 | |||
| 39 | **Mitigating factors:** | ||
| 40 | 1. Session IDs are UUIDv4 (122 bits of entropy) — practically unpredictable | ||
| 41 | 2. Attacker would need a victim's exact session cookie value | ||
| 42 | 3. No account information is leaked; only session deletion occurs | ||
| 43 | |||
| 44 | **Risk if session IDs leak:** | ||
| 45 | Session IDs could leak via: | ||
| 46 | - Server access logs (if session IDs appear in logged cookie headers) | ||
| 47 | - Error messages (currently suppressed by `unwrap_or` / `ok()` patterns) | ||
| 48 | - Database backup exposure | ||
| 49 | - Shoulder surfing / physical access | ||
| 50 | |||
| 51 | If leaked, an attacker can force-logout the victim by sending `Cookie: mcui_session=<leaked-id>` to `/logout`. | ||
| 52 | |||
| 53 | ## Pre-Auth Risk Summary | ||
| 54 | |||
| 55 | The webUI is designed to bind to `127.0.0.1` and sit behind a reverse proxy (nginx), which mitigates direct anonymous access. If exposed to the internet (even unintentionally via misconfiguration, Docker port mapping, or reverse proxy error): | ||
| 56 | |||
| 57 | | Risk | Pre-auth endpoint | Impact | Existing draft | | ||
| 58 | |------|-------------------|--------|----------------| | ||
| 59 | | **HIGH** | `POST /login` (no rate limit) | Account compromise via credential brute-force | p4-005 | | ||
| 60 | | **LOW** | `POST /logout` (missing guard) | Session deletion (requires valid session ID) | p4-008 | | ||
| 61 | | **HIGH** | All POST endpoints (no CSRF) | Cross-site request forgery on any state-changing operation | p4-003 | | ||
| 62 | | **NONE** | `GET /login` | None — static page | — | | ||
| 63 | | **NONE** | `GET /static/*` | None — only CSS/JS files | — | | ||
| 64 | |||
| 65 | ## Auth Bypass Verification | ||
| 66 | |||
| 67 | **All protected routes are correctly behind `require_auth` middleware:** | ||
| 68 | - ✓ Dashboard (`/`) | ||
| 69 | - ✓ Instances CRUD (`/instances`, `/instances/{id}/edit`, etc.) | ||
| 70 | - ✓ World management (`/instances/{id}/world/*`) | ||
| 71 | - ✓ Player management (`/players`, `/players/*`) | ||
| 72 | - ✓ Backup management (`/instances/{id}/backups`, `/backups/{id}/*`) | ||
| 73 | - ✓ Mod management (`/instances/{id}/mods`, `/instances/{id}/mods/*`) | ||
| 74 | - ✓ Account management (`/accounts`, `/accounts/*`) | ||
| 75 | - ✓ Password change (`/change-password`) | ||
| 76 | |||
| 77 | **Routes NOT behind `require_auth` (intentional for auth protocol):** | ||
| 78 | - `GET /login` — login form (by-design) | ||
| 79 | - `POST /login` — credential verification (by-design) | ||
| 80 | - `GET /static/*` — static assets (by-design) | ||
| 81 | |||
| 82 | **Route NOT behind `require_auth` (anomaly):** | ||
| 83 | - `POST /logout` — session deletion (missing-guard, p4-008) | ||
| 84 | |||
| 85 | ## Hidden Control Channel Verification | ||
| 86 | |||
| 87 | | Channel | Present? | Can it bypass auth? | | ||
| 88 | |---------|----------|---------------------| | ||
| 89 | | Cookie header (`mcui_session`) | Yes, in `require_auth` + `logout` | No — session must exist in DB | | ||
| 90 | | Content-Type boundary | Yes, in `upload_mod` handler | No — only affects file parsing | | ||
| 91 | | X-Forwarded-* headers | **Not read anywhere** | N/A (safe) | | ||
| 92 | | X-HTTP-Method-Override | **Not supported** | N/A (safe) | | ||
| 93 | | X-Admin / X-Internal / X-Debug | **Not supported** | N/A (safe) | | ||
| 94 | | Host header | **Not used for routing decisions** | N/A (safe) | | ||
| 95 | |||
| 96 | ## References to Existing Drafts | ||
| 97 | |||
| 98 | The following p4 findings document pre-auth and authz issues in detail: | ||
| 99 | |||
| 100 | | Draft | Class | Covers | | ||
| 101 | |-------|-------|--------| | ||
| 102 | | p4-003 | csrf | No CSRF on any POST endpoint | | ||
| 103 | | p4-005 | missing-rate-limit | No rate limiting on POST /login | | ||
| 104 | | p4-008 | missing-guard | POST /logout not behind auth middleware | | ||
| 105 | | p4-009 | cookie-no-secure-flag | Cookie missing Secure flag | | ||
| 106 | | p4-019 | cookie-parsing-naive | Naive cookie header string splitting | | ||
| 107 | |||
| 108 | ## Cross-Reference: Matrix Entry vs Unauthenticated Surface | ||
| 109 | |||
| 110 | All entries in this file are derived from the authorization matrix (`piolium/attack-surface/public-routes-authz-matrix.md`). The matrix covers 37 total endpoints; 4 are reachable pre-auth as documented above. The remaining 33 require authentication via `require_auth` middleware. | ||
