diff options
Diffstat (limited to 'piolium/attack-surface/public-routes-authz-matrix.md')
| -rw-r--r-- | piolium/attack-surface/public-routes-authz-matrix.md | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/piolium/attack-surface/public-routes-authz-matrix.md b/piolium/attack-surface/public-routes-authz-matrix.md new file mode 100644 index 0000000..d8f9389 --- /dev/null +++ b/piolium/attack-surface/public-routes-authz-matrix.md | |||
| @@ -0,0 +1,92 @@ | |||
| 1 | # Authorization Matrix — Public Routes & Access Control | ||
| 2 | |||
| 3 | **Coverage stats**: 37 endpoints discovered | 1 endpoint with no guard detected (missing-guard) | 24 endpoints taking object-id parameter | ||
| 4 | **Coverage gaps**: None — all routes are statically registered in the axum Router. No reflection-based or dynamically-registered handlers. | ||
| 5 | |||
| 6 | ## Legend | ||
| 7 | |||
| 8 | | Column | Description | | ||
| 9 | |--------|-------------| | ||
| 10 | | Layer-1 Guard | Declarative decorators/annotations (none in this Rust axum app) | | ||
| 11 | | In-body Authz | Handler-level checks (superuser guard, ownership filters, etc.) | | ||
| 12 | | Router/Middleware Guard | `require_auth` middleware applied at the router-group level | | ||
| 13 | | Hidden Control Channels | Request-derived values that influence auth, routing, or identity | | ||
| 14 | | Object-ID Param | Path parameter that identifies a resource (potential IDOR vector) | | ||
| 15 | | Ownership Check? | Does the handler filter queries by the acting user's identity? | | ||
| 16 | | Tenant Filter? | Does the handler filter by tenant/org scope? | | ||
| 17 | | Expected Scope | `public` / `authenticated` / `role:superuser` / `unknown` | | ||
| 18 | |||
| 19 | ## Matrix | ||
| 20 | |||
| 21 | | # | Method | Path | Handler (file:line) | Layer-1 Guard | In-body Authz | Router/Middleware Guard | Hidden Control Channels | Object-ID Param | Ownership Check? | Tenant Filter? | Expected Scope | Notes | | ||
| 22 | |---|--------|------|---------------------|---------------|---------------|-------------------------|-------------------------|------------------|------------------|----------------|----------------|-------| | ||
| 23 | | 1 | GET | `/login` | `login_page` (src/routes/mod.rs:53) | none | none | none | none | none | N/A | N/A | **public** | Login page — no session required | | ||
| 24 | | 2 | POST | `/login` | `authenticate` (src/auth.rs:34) | none | none | none | Cookie set on response, no proxy-derived identity | none | N/A | N/A | **public** | Auth entry point — credentials → session | | ||
| 25 | | 3 | POST | `/logout` | `logout` (src/auth.rs:130) | none | none | **missing guard** — not behind `require_auth` | Cookie header parsed for session ID | none | N/A | N/A | **public (anomaly: expected authenticated)** | ⚠️ Missing guard — see p4-008 | | ||
| 26 | | 4 | GET | `/static/*` | `ServeDir` (tower-http, src/routes/mod.rs:28) | none | none | none | File path after `/static/` | file path | N/A | N/A | **public** | Static files (CSS, JS only) | | ||
| 27 | | 5 | GET | `/` | `dashboard` (src/routes/dashboard.rs:19) | none | none | `require_auth` | Cookie → CurrentUser via `FromRequestParts` | none | N/A | N/A | **authenticated** | Dashboard — requires valid session | | ||
| 28 | | 6 | GET | `/instances` | `list_instances` (src/routes/instances.rs:44) | none | none | `require_auth` | Cookie → CurrentUser | none | N/A (all instances shared) | N/A (single-tenant) | **authenticated** | Lists all instances — no user scoping | | ||
| 29 | | 7 | GET | `/instances/new` | `create_form` (src/routes/instances.rs:70) | none | none | `require_auth` | Cookie → CurrentUser | none | N/A | N/A | **authenticated** | Instance creation form | | ||
| 30 | | 8 | POST | `/instances/new` | `create_instance` (src/routes/instances.rs:87) | none | none | `require_auth` | Cookie → CurrentUser | none | N/A | N/A | **authenticated** | Creates instance (name used as dir name) | | ||
| 31 | | 9 | GET | `/instances/{id}/edit` | `edit_form` (src/routes/instances.rs:129) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** — queries by id without user filter | N/A | **authenticated** | Edit form — any auth user can edit any instance | | ||
| 32 | | 10 | POST | `/instances/{id}/edit` | `edit_instance` (src/routes/instances.rs:176) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Updates any instance by id | | ||
| 33 | | 11 | POST | `/instances/{id}/delete` | `delete_instance` (src/routes/instances.rs:198) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Deletes any instance — destructive | | ||
| 34 | | 12 | POST | `/instances/{id}/activate` | `activate_instance` (src/routes/instances.rs:217) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Activates instance → Docker container start | | ||
| 35 | | 13 | POST | `/instances/deactivate` | `deactivate_instance` (src/routes/instances.rs:278) | none | none | `require_auth` | Cookie → CurrentUser | none | N/A | N/A | **authenticated** | Deactivates active instance | | ||
| 36 | | 14 | GET | `/instances/{id}/world/export` | `export_world` (src/routes/instances.rs:288) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Exports world ZIP — file read | | ||
| 37 | | 15 | POST | `/instances/{id}/world/import` | `import_world` (src/routes/instances.rs:328) | none | none | `require_auth` | Cookie → CurrentUser; Multipart Content-Type/boundary | `id` (i64, path) | **No** | N/A | **authenticated** | Imports world ZIP — file write | | ||
| 38 | | 16 | POST | `/instances/{id}/world/regenerate` | `regenerate_world` (src/routes/instances.rs:360) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Regenerates world — file write | | ||
| 39 | | 17 | GET | `/players` | `manage_players` (src/routes/players.rs:28) | none | none | `require_auth` | Cookie → CurrentUser | none | N/A | N/A | **authenticated** | Player management page | | ||
| 40 | | 18 | POST | `/players/whitelist/add` | `add_whitelist` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Cookie → CurrentUser; RCON password from DB | none (username in form) | N/A | N/A | **authenticated** | RCON command — any auth user can manage whitelist | | ||
| 41 | | 19 | POST | `/players/whitelist/remove` | `remove_whitelist` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Same as #18 | none | N/A | N/A | **authenticated** | RCON command | | ||
| 42 | | 20 | POST | `/players/op/add` | `add_op` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Same as #18 | none | N/A | N/A | **authenticated** | RCON command — any auth user can op players | | ||
| 43 | | 21 | POST | `/players/op/remove` | `remove_op` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Same as #18 | none | N/A | N/A | **authenticated** | RCON command | | ||
| 44 | | 22 | POST | `/players/ban/add` | `add_ban` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Same as #18 | none | N/A | N/A | **authenticated** | RCON command — any auth user can ban players | | ||
| 45 | | 23 | POST | `/players/ban/remove` | `remove_ban` (src/routes/players.rs:89, macro) | none | none | `require_auth` | Same as #18 | none | N/A | N/A | **authenticated** | RCON command | | ||
| 46 | | 24 | GET | `/instances/{id}/backups` | `list_backups` (src/routes/backups.rs:30) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Lists backups for any instance | | ||
| 47 | | 25 | POST | `/instances/{id}/backups/create` | `create_backup` (src/routes/backups.rs:54) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Creates backup — file write | | ||
| 48 | | 26 | POST | `/backups/{id}/restore` | `restore_backup` (src/routes/backups.rs:88) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | STUB — no-op redirect | | ||
| 49 | | 27 | GET | `/backups/{id}/download` | `download_backup` (src/routes/backups.rs:93) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Downloads backup ZIP — file read | | ||
| 50 | | 28 | GET | `/instances/{id}/mods` | `list_mods` (src/routes/mods.rs:42) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Lists mods for any instance | | ||
| 51 | | 29 | POST | `/instances/{id}/mods` | `upload_mod` (src/routes/mods.rs:86) | none | none | `require_auth` | Cookie → CurrentUser; Content-Type boundary for multer | `id` (i64, path) | **No** | N/A | **authenticated** | Uploads mod — file write, JAR parsing | | ||
| 52 | | 30 | POST | `/instances/{id}/mods/{mod_id}/delete` | `delete_mod` (src/routes/mods.rs:188) | none | none | `require_auth` | Cookie → CurrentUser | `instance_id`, `mod_id` (path) | **No** | N/A | **authenticated** | Deletes mod — file delete | | ||
| 53 | | 31 | POST | `/instances/{id}/mods/rescan` | `rescan_mods` (src/routes/mods.rs:163) | none | none | `require_auth` | Cookie → CurrentUser | `id` (i64, path) | **No** | N/A | **authenticated** | Rescans mods directory — file read + DB insert | | ||
| 54 | | 32 | GET | `/accounts` | `manage_accounts` (src/routes/accounts.rs:60) | none | `if !account.is_superuser` (line 107) | `require_auth` | Cookie → CurrentUser | none | N/A | N/A | **role:superuser** | Account list — superuser inline check | | ||
| 55 | | 33 | POST | `/accounts/create` | `create_account` (src/routes/accounts.rs:95) | none | `if !account.is_superuser` (line 145) | `require_auth` | Cookie → CurrentUser | none (form fields) | N/A | N/A | **role:superuser** | Create account — superuser inline check | | ||
| 56 | | 34 | POST | `/accounts/delete` | `delete_account` (src/routes/accounts.rs:111) | none | `if !account.is_superuser` (line 167) | `require_auth` | Cookie → CurrentUser | `form.id` (body) | N/A | N/A | **role:superuser** | Delete account — superuser inline check | | ||
| 57 | | 35 | POST | `/accounts/set-password` | `set_password` (src/routes/accounts.rs:142) | none | `if !account.is_superuser` (line 204) | `require_auth` | Cookie → CurrentUser | `form.id` (body) | N/A | N/A | **role:superuser** | Set password — superuser inline check | | ||
| 58 | | 36 | GET | `/change-password` | `change_password_page` (src/routes/mod.rs:58) | none | none | `require_auth` | Cookie → CurrentUser (`FromRequestParts`) | none | N/A | N/A | **authenticated** | Password change page — requires auth | | ||
| 59 | | 37 | POST | `/change-password` | `accounts::change_password` (src/routes/accounts.rs:163) | none | none | `require_auth` | Cookie → CurrentUser | none | Self (uses `account.id` from auth) | N/A | **authenticated** | Change own password — verifies current password | | ||
| 60 | | 38 | POST | `/accounts/change-own-password` | `change_own_password_from_accounts` (src/routes/accounts.rs:163) | none | none | `require_auth` | Cookie → CurrentUser | none | Self (uses `account.id` from auth) | N/A | **authenticated** | Duplicate of #37, same handler | | ||
| 61 | |||
| 62 | ## Anomaly Summary | ||
| 63 | |||
| 64 | | # | Anomaly | Routes Affected | Severity | Existing Draft | | ||
| 65 | |---|---------|-----------------|----------|----------------| | ||
| 66 | | A1 | **Missing guard** — POST /logout registered on outer router, not behind `require_auth` middleware | #3 | MEDIUM | p4-008 | | ||
| 67 | | A2 | **Fragile superuser pattern** — superuser check done inline in handler body via `if !account.is_superuser` instead of middleware/extractor | #32, #33, #34, #35 | LOW (pattern concern) | p4-025 | | ||
| 68 | | A3 | **No ownership/tenant scoping** — all authenticated users have equal access to ALL instances, mods, backups, player management | #6–#31 | INFO (by design: single-user/small-team) | — | | ||
| 69 | | A4 | **No CSRF protection** — all POST endpoints lack CSRF tokens | #2, #3, #8, #10, #11, #12, #13, #15, #16, #18–#23, #25, #26, #29, #30, #31, #33–#35, #37, #38 | HIGH | p4-003 | | ||
| 70 | | A5 | **Hidden control channel** — `Content-Type` boundary parsed for multer multipart (attacker-controlled boundary string) | #29 | LOW | — | | ||
| 71 | | A6 | **Hidden control channel** — Cookie header parsing relies on raw string splitting (no validation of session ID format) | All protected routes | LOW | p4-019 | | ||
| 72 | |||
| 73 | ## Hidden Control Channels Map | ||
| 74 | |||
| 75 | | Channel | Where Used | What It Controls | Can It Bypass Auth? | | ||
| 76 | |---------|-----------|-----------------|---------------------| | ||
| 77 | | `Cookie: mcui_session` | `require_auth` (src/auth.rs:55), `logout` (src/auth.rs:130) | Identity (session lookup → Account) | No — session must exist in DB | | ||
| 78 | | `Content-Type: boundary=` | `upload_mod` (src/routes/mods.rs:86) | Multipart parsing boundary | No — only affects file parsing | | ||
| 79 | | Path parameters `{id}` | All `/instances/{id}/*`, `/backups/{id}/*` routes | Resource selection | No — but no ownership check on selected resource | | ||
| 80 | | Form fields | All POST handlers | Operation parameters | No — parametrized queries prevent injection | | ||
| 81 | | X-Forwarded-* Headers | **Not read anywhere** | N/A | N/A (safe — not trusted) | | ||
| 82 | | X-HTTP-Method-Override | **Not supported** | N/A | N/A (safe) | | ||
| 83 | | X-Admin / X-Internal | **Not supported** | N/A | N/A (safe) | | ||
| 84 | |||
| 85 | ## Authz Architecture Summary | ||
| 86 | |||
| 87 | - **Authentication**: Cookie-based session (UUIDv4) via `require_auth` middleware (src/auth.rs:55) | ||
| 88 | - **Authorization model**: Flat — all authenticated users are equal. No per-resource ownership, no roles beyond `is_superuser`. | ||
| 89 | - **Superuser gating**: Inline `if !account.is_superuser` checks in handler bodies (src/routes/accounts.rs) | ||
| 90 | - **Static file serving**: Completely public | ||
| 91 | - **Logout**: Public (missing guard — only by design because session IDs are unpredictable UUIDs) | ||
| 92 | - **Session management**: 24h max-age cookie, NO server-side TTL check, NO Secure flag | ||
