---
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
