---
phase: 8
slug: stack-upgrade-backend-decomposition
status: verified
threats_open: 0
asvs_level: 2
created: 2026-06-12
---

# Phase 8 — Security

> Per-phase security contract: threat register, accepted risks, and audit trail.

---

## Trust Boundaries

| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| client → POST /api/auth/change-password | Authenticated user changes password; backend revokes all OTHER refresh tokens | JWT access token (Bearer); password plaintext (TLS-only) |
| client → POST /api/auth/totp/enable | Authenticated user enables TOTP; backend revokes all OTHER refresh tokens | JWT access token; TOTP code |
| client → DELETE /api/auth/totp | Authenticated user disables TOTP; backend revokes all OTHER refresh tokens | JWT access token; TOTP code |
| api/cloud.py → api/schemas.py | Module-load-time import; no runtime data crosses | Class reference only |
| Admin list-cloud-connections → CloudConnectionOut | SEC-08: credentials_enc deliberately excluded from schema | Sanitized connection metadata |
| Vue component → Pinia toast store | In-process function call | Hardcoded string literal (no user input) |
| Backend response (sessions_revoked) → frontend toast trigger | Backend integer validated server-side; frontend uses boolean test only | Integer (count only) |
| Unauthenticated client → /api/auth/login, /api/auth/register, /api/auth/refresh | Rate-limited via @limiter.limit("10/minute") | Credentials (TLS-only) |
| Authenticated user → /api/auth/refresh | Refresh-token rotation + family-revocation on reuse | httpOnly Strict cookie |
| Browser → backend API | All requests carry Bearer token from authStore memory only | JWT access token (never localStorage) |
| Browser → domain API modules | In-process import; request() reads authStore.accessToken | Access token (Pinia memory) |
| npmjs.com → frontend node_modules | 8 PERF-01 packages installed | Bundled JS (supply chain) |
| pypi.org → backend venv | requirements.txt exact-pinned via D-17 | Python packages (supply chain) |

---

## Threat Register

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-08-01-01 | Tampering | Test fixture isolation | mitigate | Each CR test creates its own user via `_register_user()`/`_login_session()` helpers; `revoke_client` fixture provides isolated `FakeRedis` + `db_session` per test | closed |
| T-08-01-02 | Repudiation | xfail strict mode | mitigate | `@pytest.mark.xfail` removed in plan 08-03; all three CR tests run strictly | closed |
| T-08-01-SC | Supply Chain | pytest, pyotp | accept | Already pinned in requirements.txt (`pytest==9.0.3`, `pyotp==2.9.0`) | closed |
| T-08-02-01 | Information Disclosure | CloudConnectionOut schema | mitigate | Field whitelist in `api/schemas.py:14-42`; `credentials_enc` confirmed absent | closed |
| T-08-02-02 | Tampering | id coercion | mitigate | `@field_validator("id", mode="before") coerce_id_to_str` preserved verbatim at `api/schemas.py:38-42` | closed |
| T-08-02-03 | Denial of Service | Duplicate class definitions | mitigate | Old `CloudConnectionOut` in `api/admin.py` deleted in plan 08-04; `api/cloud.py:35` imports only from `api.schemas` | closed |
| T-08-02-SC | Supply Chain | No new packages | accept | Plan 08-02 installs zero new packages | closed |
| T-08-03-01 | Spoofing | Toast message injection | mitigate | Toast message is a string literal in `SettingsAccountTab.vue:204,240` and `TotpEnrollment.vue:156`; no user-controlled content | closed |
| T-08-03-02 | Repudiation | Audit log for revoked sessions | mitigate | `write_audit_log` with `sessions_revoked` metadata preserved in `api/auth/password.py:82-89`, `api/auth/totp.py:93-103`, `api/auth/totp.py:141-148` | closed |
| T-08-03-03 | Information Disclosure | Toast leaks session count | accept | UI shows generic "Other sessions have been terminated."; exact count not disclosed | closed |
| T-08-03-04 | Tampering | xfail strict=False masks regression | mitigate | Zero `@pytest.mark.xfail` decorators remain in `backend/tests/test_auth.py` | closed |
| T-08-03-SC | Supply Chain | No new packages | accept | Plan 08-03 installs zero new packages | closed |
| T-08-04-01 | Elevation of Privilege | Admin sub-router auth gate | mitigate | Every handler in `users.py` (7), `quotas.py` (2), `ai.py` (4) has `_admin: User = Depends(get_current_admin)` | closed |
| T-08-04-02 | Tampering | Sub-router prefix doubling | mitigate | Sub-routers carry no prefix; sole prefix `/api/admin` in `api/admin/__init__.py:20` | closed |
| T-08-04-03 | Information Disclosure | _user_to_dict field set | mitigate | `api/admin/shared.py:13-28` returns 10 whitelisted fields; `password_hash`, `credentials_enc`, `totp_secret` absent | closed |
| T-08-04-04 | Denial of Service | Circular import via __init__.py | mitigate | `api/admin/__init__.py` — only `APIRouter` creation and three `include_router` calls | closed |
| T-08-04-05 | Tampering | Validator duplication | mitigate | `provider_must_be_known` delegates entirely to `services.ai_config.validate_provider_id` (`api/admin/ai.py:25,72-73,93-94`) | closed |
| T-08-04-06 | Information Disclosure | Old admin.py left in place | mitigate | `backend/api/admin.py`, `backend/api/documents.py`, `backend/api/auth.py` — all three old monolith files deleted | closed |
| T-08-04-SC | Supply Chain | No new packages | accept | Plan 08-04 installs zero new packages | closed |
| T-08-05-01 | Elevation of Privilege | IDOR on document endpoints | mitigate | Ownership assertion `doc.user_id != current_user.id` → 404 preserved on all handlers: `crud.py:199-201,250-251,306-307,379-380`, `upload.py:289-291`, `content.py:89` | closed |
| T-08-05-02 | Tampering | Sub-router prefix doubling | mitigate | Sub-routers carry no prefix; sole prefix `/api/documents` in `api/documents/__init__.py:21` | closed |
| T-08-05-03 | Information Disclosure | DocumentPatch.filename validator | mitigate | `filename_no_path_separators` raises `ValueError` on `/` or `\` in `api/documents/shared.py:40-45` | closed |
| T-08-05-04 | Tampering | Atomic quota UPDATE invariant | mitigate | Atomic `UPDATE quotas SET used_bytes = used_bytes + $delta WHERE … RETURNING` preserved in `upload.py:310-316`; atomic decrement in `services/storage.py:179-186` | closed |
| T-08-05-05 | Denial of Service | Circular import via __init__.py | mitigate | `api/documents/__init__.py` — only aggregation | closed |
| T-08-05-SC | Supply Chain | No new packages | accept | Plan 08-05 installs zero new packages | closed |
| T-08-06-01 | Spoofing | limiter re-export | mitigate | `api/auth/__init__.py:13` re-exports the same `Limiter` instance from `shared.py:22`; identity confirmed via `app.state.limiter is api.auth.limiter` | closed |
| T-08-06-02 | Repudiation | CR-01/CR-02/CR-03 audit log entries | mitigate | `write_audit_log` with `sessions_revoked` metadata preserved on all three handlers in `password.py` and `totp.py` | closed |
| T-08-06-03 | Tampering | Refresh-token rotation logic | mitigate | JTI, fgp, family-revocation on reuse all present in `services/auth.py:114-115,233-239` | closed |
| T-08-06-04 | Information Disclosure | Sub-router prefix doubling | mitigate | Sub-routers carry no prefix; sole prefix `/api/auth` in `api/auth/__init__.py:15` | closed |
| T-08-06-05 | Denial of Service | Circular import via __init__.py | mitigate | `api/auth/__init__.py` — only aggregation | closed |
| T-08-06-06 | Elevation of Privilege | Session revocation skip | mitigate | `skip_token_hash=skip_hash` preserved in `password.py:79-80`, `totp.py:89-90` (enable), `totp.py:136-137` (disable) | closed |
| T-08-06-SC | Supply Chain | No new packages | accept | Plan 08-06 installs zero new packages | closed |
| T-08-07-01 | Spoofing | Bearer token injection | mitigate | `utils.js:31-37` lazy-imports authStore and reads `authStore.accessToken`; pattern preserved in `fetchWithRetry:97-102` | closed |
| T-08-07-02 | Tampering | Circular import producing undefined exports | mitigate | `request` lives in `utils.js`; all domain modules (`documents.js:8`, `auth.js:8`, etc.) import from `utils.js` | closed |
| T-08-07-03 | Repudiation | 401-refresh-retry loop | mitigate | `_retry` flag preserved in `request()` (`utils.js:45`) and `fetchWithRetry()` (`utils.js:107`); recursion bounded | closed |
| T-08-07-04 | Information Disclosure | Token stored in JS storage | mitigate | `utils.js:35-36` reads `authStore.accessToken` (Pinia memory only); zero `localStorage`/`sessionStorage` references | closed |
| T-08-07-05 | Tampering | export * name collision | mitigate | All 7 domain module export names verified unique; frontend build + test suite would fail on collision | closed |
| T-08-07-06 | Denial of Service | Consumer files break | mitigate | `client.js:13-20` — `export *` from each domain module + explicit re-exports from `utils.js`; 36 consumer files confirmed untouched | closed |
| T-08-07-SC | Supply Chain | No new npm packages | accept | Plan 08-07 installs zero new packages | closed |
| T-08-08-01 | Supply Chain (Tampering) | 8 npm package installs | mitigate | Human checkpoint (task 1) required; packages verified on npmjs.com; `package.json:12-19` confirms all 8 present | closed |
| T-08-08-02 | Tampering | Vite 6 migration breaking behavior | mitigate | No breaking changes apply (no custom resolve.conditions, no Sass, no library mode); `package.json:30` — `"vite": "^6.4.3"`; npm test + build pass | closed |
| T-08-08-03 | Information Disclosure | requirements.txt leaks versions | accept | Version visibility in requirements.txt accepted; reproducibility benefit outweighs disclosure | closed |
| T-08-08-04 | Denial of Service | alembic version discrepancy | mitigate | `requirements.txt:18-19` — `alembic==1.16.5` with inline comment documenting the `>=1.18.4` → `1.16.5` discrepancy for future deliberate bump | closed |
| T-08-08-05 | Tampering | @vitejs/plugin-vue peer-dep mismatch | mitigate | Both `vite@^6` and `@vitejs/plugin-vue@^6` installed together per RESEARCH.md §Pitfall 6; `package.json:23,30` confirms major v6 alignment | closed |
| T-08-08-SC | Supply Chain | npm packages | mitigate | Human checkpoint is the gate; packages verified; SUMMARY.md confirms pass | 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-08-01 | T-08-01-SC | pytest/pyotp already pinned in requirements.txt before this phase | project owner | 2026-06-12 |
| AR-08-02 | T-08-02-SC | Plan 08-02 adds no new packages | project owner | 2026-06-12 |
| AR-08-03 | T-08-03-03 | Toast shows generic message only; session count not disclosed to UI | project owner | 2026-06-12 |
| AR-08-04 | T-08-03-SC | Plan 08-03 adds no new packages | project owner | 2026-06-12 |
| AR-08-05 | T-08-04-SC | Plan 08-04 adds no new packages | project owner | 2026-06-12 |
| AR-08-06 | T-08-05-SC | Plan 08-05 adds no new packages | project owner | 2026-06-12 |
| AR-08-07 | T-08-06-SC | Plan 08-06 adds no new packages | project owner | 2026-06-12 |
| AR-08-08 | T-08-07-SC | Plan 08-07 adds no new npm packages | project owner | 2026-06-12 |
| AR-08-09 | T-08-08-03 | Package versions in requirements.txt are not secrets; reproducibility benefit accepted | project owner | 2026-06-12 |

---

## Security Audit Trail

| Audit Date | Threats Total | Closed | Open | Run By |
|------------|---------------|--------|------|--------|
| 2026-06-12 | 45 | 45 | 0 | gsd-security-auditor (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
