Files
kite/.planning/milestones/v0.2-phases/11-visual-design-responsive-layout-cleanup/11-SECURITY.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

11 KiB

phase, slug, status, threats_open, asvs_level, created
phase slug status threats_open asvs_level created
11 visual-design-responsive-layout-cleanup complete 0 1 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.jsstats.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.