---
phase: 11
slug: visual-design-responsive-layout-cleanup
status: complete
threats_open: 0
asvs_level: 1
created: 2026-06-17
---

# Phase 11 — Security

> Per-phase security contract: threat register, accepted risks, and audit trail.
> Mode: retroactive-STRIDE — no formal threat model existed at authoring time; register built from implementation.

---

## Trust Boundaries

| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| Browser / Vue Router | Navigation guard enforces auth and role separation before any route component resolves | JWT access token (memory), user role claim |
| Browser / API | Document content fetched via authenticated API client using httpOnly cookie; rendered into sandboxed blob URL iframe | Document binary content |
| Build pipeline / Production bundle | `stats.html` bundle visualizer written only when `ANALYZE=true`; gitignored; never shipped in `dist/` | Source file names, module sizes |
| Admin layout / User layout | `AdminLayout.vue` registered exclusively on the `/admin` route subtree, which carries `requiresAdmin: true` meta | Admin-only navigation structure |

---

## Threat Register

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| S-01 | Spoofing | `router/index.js` — lazy route resolution | mitigate | Vue Router 4 `beforeEach` is async and fully resolves before any component is loaded or mounted. Guard at line 93 attempts `authStore.refresh()` before allowing any non-public route. Lazy loading (Plan 11-02) does not change guard sequencing. Verified: router guard tests in `router.guard.test.js` mock all lazy components and confirm guard fires before resolution. | CLOSED |
| S-02 | Spoofing | `App.vue` — hamburger header renders before route settles | mitigate | The hamburger header is inside the `v-else` branch (line 4 of App.vue template), which only renders after `route.meta.layout !== 'auth'` AND `!route.matched.some(r => r.meta.requiresAdmin)`. An unauthenticated navigation is redirected to `/login` before the component tree mounts. Header reveals no session-identifying data; it shows only the app name. | CLOSED |
| S-03 | Spoofing | `AdminLayout.vue` — admin drawer implies admin role | mitigate | `AdminLayout.vue` is only reachable via the `/admin` route subtree (router `index.js` line 47). That route has `meta: { requiresAdmin: true }` (line 48). The guard at lines 104-110 confirms `authStore.user?.role === 'admin'` via `to.matched.some()`; non-admins are redirected to `/`. Tested in `router.guard.test.js` lines 115-135 (child route inheritance). | CLOSED |
| T-01 | Tampering | All Phase 11 templates — content injection via `v-html` | mitigate | Grep over all 10 Phase 11 implementation files finds zero uses of `v-html`, `innerHTML`, or `eval`. All user-data interpolation uses Vue template double-brace syntax (`{{ }}`), which auto-escapes. Folder names, file names, error messages, and share handles are all rendered via text interpolation only. | CLOSED |
| T-02 | Tampering | `StorageBrowser.vue` — drag-and-drop DOM manipulation | accept | The drag-and-drop implementation uses native `dataTransfer.setData('text/plain', file.id)` (line 349). The received `fileId` on drop is a UUID read from the in-memory file object, not from `dataTransfer.getData()`. No untrusted DOM string is used to construct API calls. Accepted: the drag event does not introduce a new injection surface. | CLOSED |
| R-01 | Repudiation | All Phase 11 changes | accept | Phase 11 introduces no event logging, audit trail, or backend changes. Repudiation threats are not applicable to pure CSS/layout frontend changes. No user actions introduced in this phase bypass existing backend audit logging (all document/folder mutations still go through the authenticated API). | CLOSED |
| I-01 | Information Disclosure | `StorageBrowser.vue` — CSS-hidden responsive columns | accept | The `hidden sm:block` / `hidden md:block` classes (lines 41-42, 97-98, 144-145) hide "Size" and "Modified" metadata columns on narrow viewports. These values are already rendered in the DOM and inspectable via browser DevTools. They contain file size bytes and ISO dates — no access-controlled content, PII, or credentials. All data in this component is already visible in the authenticated user's own file list. Risk accepted: CSS column hiding is a display optimization, not a security boundary. | CLOSED |
| I-02 | Information Disclosure | `vite.config.js` — `stats.html` bundle analyzer output | mitigate | The visualizer is conditionally activated only when `ANALYZE=true` is set (strict string equality at line 8 of `vite.config.js`). `frontend/stats.html` is listed in `.gitignore` (confirmed at line 8 of root `.gitignore`). `frontend/dist/` is also gitignored (line 6). The output file is placed in the project root (not `dist/`), so it cannot be served by the production Vite dev server or shipped in the build artifact. | CLOSED |
| I-03 | Information Disclosure | `AppSidebar.vue` — admin navigation link visible to non-admins via DOM | accept | The Admin link in `AppSidebar.vue` is conditionally rendered with `v-if="authStore.user?.role === 'admin'"` (line 169). `v-if` removes the DOM node entirely for non-admin users — it is not CSS-hidden. Even if it were CSS-hidden, the link itself only navigates to `/admin`, which the router guard blocks. No admin data or content is exposed. | CLOSED |
| I-04 | Information Disclosure | `DocumentPreviewModal.vue` — document content in iframe | mitigate | Document content is fetched via `fetchDocumentContent(docId)` using the project's authenticated API client (bearer token injected per `client.js`). The response blob is wrapped in `URL.createObjectURL()` and assigned to a sandboxed `<iframe>` as the `src`. No raw API URL with credentials is ever placed in the iframe `src`. Blob URLs are revoked on modal close (`onUnmounted`, line 131). | CLOSED |
| D-01 | Denial of Service | `router/index.js` — lazy-load chunk fetch failure | accept | If a lazy route chunk fails to load (network error, CDN outage), Vue Router raises a `NavigationFailure`. The application does not crash; the user remains on the current route. No error recovery handler was added in Phase 11 — this matches pre-existing behavior. Accepted: chunk load failure is an infrastructure concern outside frontend scope; a future phase may add a global `router.onError` handler. This is not a regression introduced by Phase 11. | CLOSED |
| D-02 | Denial of Service | `App.vue` / `AdminLayout.vue` — drawer stuck open blocking navigation | mitigate | `drawerOpen` is set to `false` in a `watch(() => route.fullPath, ...)` handler (App.vue line 64-66; AdminLayout.vue line 57-59). Any successful navigation closes the drawer. The backdrop click (`@click="drawerOpen = false"`) provides a secondary close path. The drawer is CSS `translate-x` based — it never removes underlying DOM elements, so keyboard navigation and router-links behind the drawer remain accessible. | CLOSED |
| E-01 | Elevation of Privilege | `router/index.js` + `AdminLayout.vue` — regular user reaching admin UI | mitigate | Three independent layers enforce admin-only access: (1) Router guard `beforeEach` checks `to.matched.some(r => r.meta.requiresAdmin)` and `authStore.user?.role === 'admin'` before allowing navigation (lines 104-110). (2) `AdminLayout.vue` is only registered as the component for the `/admin` route subtree. (3) `AppSidebar.vue` renders the Admin link only when `role === 'admin'` (v-if removes it from DOM). Privilege elevation via the admin drawer requires passing the router guard, which verifies the role claim from the server-issued JWT. | CLOSED |
| E-02 | Elevation of Privilege | `App.vue` — admin user accessing regular user drawer | accept | The router guard (lines 112-115) redirects admin users away from non-admin, non-public routes to `/admin`. If an admin reaches the regular layout branch (e.g. visiting `/` directly), the `v-else` branch of `App.vue` renders `AppSidebar`, which includes an Admin link. This is the intended behavior — admins are separated by redirect, not by blocking the regular layout entirely. Accepted: the regular layout is not a security boundary for admins; the admin role check in the guard is. | CLOSED |

---

## Accepted Risks Log

| Risk ID | Threat Ref | Rationale | Accepted By | Date |
|---------|------------|-----------|-------------|------|
| AR-01 | T-02 | Drag-and-drop uses in-memory object IDs, not DOM-sourced strings; no injection surface created | security-auditor | 2026-06-17 |
| AR-02 | R-01 | Phase 11 is pure frontend visual changes; no new user actions bypass backend audit logging | security-auditor | 2026-06-17 |
| AR-03 | I-01 | CSS-hidden columns contain file size and date only — non-sensitive metadata already visible in the authenticated session | security-auditor | 2026-06-17 |
| AR-04 | I-03 | Admin nav link is v-if removed from DOM for non-admins; router guard is the authoritative enforcement layer | security-auditor | 2026-06-17 |
| AR-05 | D-01 | Lazy chunk load failure is a pre-existing infrastructure risk; not a regression; deferred to a future hardening phase | security-auditor | 2026-06-17 |
| AR-06 | E-02 | Admin users reaching the regular layout see the Admin sidebar link (correct behavior); redirect is the separation mechanism, not layout blocking | security-auditor | 2026-06-17 |

---

## Audit Trail

### Security Audit 2026-06-17

| Metric | Count |
|--------|-------|
| Threats found | 13 |
| Closed | 13 |
| Open | 0 |
| Mode | retroactive-STRIDE |
| ASVS level | 1 (baseline) |
| Block on | HIGH severity unmitigated threats |
| Files inspected | 10 implementation files + 2 test files + 3 plan summaries |

**Key findings:**

- No `v-html` usage anywhere in Phase 11 modified files — XSS via template injection is not possible.
- Auth guard in Vue Router 4 runs before lazy-loaded component resolution — lazy loading (Plan 11-02) does not create a window where admin routes are accessible without role verification.
- The `stats.html` bundle analyzer output is correctly gitignored and never included in `dist/`; it cannot be served in production.
- CSS-hidden responsive columns (`hidden sm:block`) contain only file size and date metadata — no access-controlled content or credentials.
- `AdminLayout.vue` is exclusively registered on the `/admin` route subtree; the guard enforces `requiresAdmin` via `to.matched.some()` which propagates to all child routes without requiring per-child meta re-declaration.
- `DocumentPreviewModal.vue` uses authenticated blob URLs for iframe content — no credential leakage via iframe `src`.
- Drawer state cannot block navigation: route-change watcher auto-closes it, and the CSS translate approach keeps underlying DOM accessible.
