From c4adf9a990a38d789cd96a65098181ee2233eab9 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Fri, 12 Jun 2026 22:28:15 +0200 Subject: [PATCH] =?UTF-8?q?docs(phase-09):=20add=20security=20threat=20ver?= =?UTF-8?q?ification=20=E2=80=94=2015/15=20threats=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../09-SECURITY.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .planning/phases/09-admin-panel-rearchitecture/09-SECURITY.md diff --git a/.planning/phases/09-admin-panel-rearchitecture/09-SECURITY.md b/.planning/phases/09-admin-panel-rearchitecture/09-SECURITY.md new file mode 100644 index 0000000..8f9692b --- /dev/null +++ b/.planning/phases/09-admin-panel-rearchitecture/09-SECURITY.md @@ -0,0 +1,79 @@ +--- +phase: 09 +slug: admin-panel-rearchitecture +status: verified +threats_open: 0 +asvs_level: 1 +created: 2026-06-12 +--- + +# Phase 09 — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| browser → `/api/admin/overview` | Admin JWT crosses; aggregate stats + audit rows returned | Aggregate counts, whitelisted audit log rows (non-sensitive) | +| Vue runtime → backend admin API | `getAdminOverview()` call via shared `request()` helper; relies on existing auth + refresh flow | Admin stats payload | +| Tab-to-view extraction | Purely structural; no new ingress, egress, or trust transitions | None | +| Browser address bar → `router.beforeEach` guard | Untrusted URL crosses; guard decides whether to mount admin chrome | Route metadata only | +| Comment purge → invariant erasure | Code that depends on a constraint may silently break if the constraint comment is removed | None — code-only operation | + +--- + +## Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation | Status | +|-----------|----------|-----------|-------------|------------|--------| +| T-09-01-01 | Elevation of Privilege | `GET /api/admin/overview` | mitigate | `_admin: User = Depends(get_current_admin)` raises 401/403 for non-admin tokens; covered by `test_overview_requires_admin` + `test_overview_non_admin_forbidden` | closed | +| T-09-01-02 | Information Disclosure | `GET /api/admin/overview` response | mitigate | Hand-rolled dict with only aggregate counts + whitelisted `_audit_to_dict_with_handles` rows; no `password_hash`/`credentials_enc`/`extracted_text`/`totp_secret`/`api_key_enc` ever serialized; covered by `test_overview_no_sensitive_fields` | closed | +| T-09-01-03 | Tampering | `_build_filtered_query_with_handles` import | accept | Cross-module import from sibling `api/audit.py` is a stable existing helper already security-audited and tested; risk accepted because an ImportError fails loud on startup | closed | +| T-09-02-01 | Information Disclosure | `AdminOverviewView` render | accept | Component renders only fields returned by backend; backend whitelist (T-09-01-02) is the authoritative gate; no `v-html` or `innerHTML`; Vue auto-escaping handles XSS | closed | +| T-09-02-02 | Elevation of Privilege | `AdminLayout` shown to non-admin | mitigate | Router guard updated in 09-04 (`to.matched.some(r => r.meta.requiresAdmin)`) ensures layout never mounts for non-admin users | closed | +| T-09-02-03 | Spoofing | Sidebar `authStore.logout()` | accept | Reuses existing logout flow audited in Phase 7.1; no new code path | closed | +| T-09-03-01 | Tampering | Tab-to-view extraction | mitigate | Verbatim copy of template + script preserves behavior; `npm run build` catches resolution errors; line-count check ±10% verifies no accidental edits | closed | +| T-09-03-02 | Information Disclosure | Orphaned emit handlers | mitigate | Action step audits every `emit(...)` for orphaned parent listeners and replaces them with direct re-fetches so no admin action silently no-ops | closed | +| T-09-04-01 | Elevation of Privilege | `beforeEach` guard | mitigate | `to.matched.some(r => r.meta.requiresAdmin)` covers all child routes; backend `get_current_admin` deps on `/api/admin/*` are the authoritative second gate | closed | +| T-09-04-02 | Privilege Escalation | `LoginView` `?redirect=` query param | mitigate | D-08 puts role check FIRST; even `?redirect=/` for an admin routes through the D-09 guard — redirect manipulation cannot grant access to the wrong area | closed | +| T-09-04-03 | Information Disclosure | Vite production build CSS purge | mitigate | Tailwind safelist covers `sky` (OneDrive) and `amber` (audit admin badge); build output confirms separate admin chunks; dynamic classes survive purge | closed | +| T-09-04-04 | Denial of Service | D-09 admin redirect loop | mitigate | `isAdminRoute` short-circuit for `/admin/*` prevents admins on admin routes from being redirected back to `/admin`; auth-await before both guard branches prevents race on token refresh | closed | +| T-09-05-01 | Tampering | Constraint comment erasure | mitigate | Explicit preservation list in purge task: NO-prefix invariants, HKDF domain separation, constant-time comparison, atomic UPDATE-RETURNING; grep assertions confirm anchor comments present post-purge | closed | +| T-09-05-02 | Denial of Service | Accidental import removal during purge | mitigate | Post-purge `python -c "from api.admin import router; …"` import check run; full `pytest -v` is the second gate | closed | +| T-09-05-03 | Information Disclosure | Comment leaking implementation details | accept | Purge removes more than it adds; no new comments introduced; existing WHY comments already reviewed in Phase 8 security agent runs | closed | + +*Status: open · closed* +*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)* + +--- + +## Accepted Risks Log + +| Risk ID | Threat Ref | Rationale | Accepted By | Date | +|---------|------------|-----------|-------------|------| +| AR-09-01 | T-09-01-03 | `_build_filtered_query_with_handles` import from sibling `api/audit.py` is a stable, security-audited helper. A future refactor breaking it will produce a loud `ImportError` at startup, not a silent security failure. | curo1305 | 2026-06-12 | +| AR-09-02 | T-09-02-01 | `AdminOverviewView` renders only backend-returned fields. Backend whitelist (T-09-01-02) is the authoritative disclosure gate. Vue template auto-escaping prevents XSS. | curo1305 | 2026-06-12 | +| AR-09-03 | T-09-02-03 | Sidebar logout reuses the Phase 7.1 logout flow with no new code path. The existing implementation is already security-audited. | curo1305 | 2026-06-12 | +| AR-09-04 | T-09-05-03 | Comment purge removes WHAT comments; no new comments introduced. WHY/security-invariant comments were verified present post-purge. Net effect is reduced information leakage, not increased. | curo1305 | 2026-06-12 | + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| 2026-06-12 | 15 | 15 | 0 | gsd-secure-phase (claude-sonnet-4-6) | + +--- + +## Sign-Off + +- [x] All threats have a disposition (mitigate / accept / transfer) +- [x] Accepted risks documented in Accepted Risks Log +- [x] `threats_open: 0` confirmed +- [x] `status: verified` set in frontmatter + +**Approval:** verified 2026-06-12