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