Files
familysync/.planning/milestones/v1.0-phases/03-event-write-back-pwa-install/03-SECURITY.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

13 KiB

phase, slug, status, threats_open, asvs_level, created
phase slug status threats_open asvs_level created
03 event-write-back-pwa-install verified 0 1 2026-06-09

Phase 03 — Security

Per-phase security contract: threat register, accepted risks, and audit trail. Verified against the CURRENT implementation, i.e. after the code-review fix cycle (CR-01/CR-02 member-scoped lookups, CR-01 move-path RRULE forwarding, IN-03 worker payload re-validation, WR-04 worker-startup gate) — not the as-executed SUMMARY claims.


Trust Boundaries

Boundary Description Data Crossing
Browser ↔ API PWA calls Hono API over HTTPS (Pangolin/Newt tunnel) Event field JSON, session cookie; no etag/credentials from client
OIDC (Authelia) ↔ API Authorization-code + PKCE; storage-less JWT session cookie iss/sub identity claims
Dev-bypass ↔ API DEV_AUTH_BYPASS=true AND NODE_ENV!=production injects a fixed dev user Local dev only; hard-OFF in production
API ↔ MariaDB Drizzle/mysql2 parameterized queries Event cache, outbox rows, encrypted app passwords
Outbox worker ↔ Fastmail CalDAV Background worker PUT/DELETE with server-sourced etag (If-Match) VEVENT payloads; decrypted app password (never logged)
Service Worker ↔ network Workbox SW; /callback, /api, /health on navigateFallbackDenylist; runtimeCaching: [] No authenticated API responses cached; OIDC callback never SW-served

Threat Register

Threat ID Category Component Disposition Mitigation Status
T-03-01 Tampering drizzle-kit push mitigate Human checkpoint + hand-applied additive DDL; runtime CMD is node dist/index.js (Dockerfile:46); db:push manual-only npm script closed
T-03-02 Info Disclosure calendar_outbox payload/etag accept Outbox rows are server-side only; never returned to the frontend closed
T-03-03 Tampering VEVENT field serialization mitigate ical.js ICAL.Component/Property/Recur for all serialization; no hand-rolled ICS (vevent.ts:89-148) closed
T-03-04 Spoofing etag forgery to bypass conflict mitigate etag sourced server-side from calendarEvents.etag; never read from request body (write.ts:62-86, outboxWorker.ts:264-279) closed
T-03-05 EoP write.ts called w/ another member's calendar accept Low-level primitive; ownership enforced at the route layer (T-03-06) closed
T-03-06 EoP write to another member's personal calendar mitigate Route lookup scoped and(eq(uid), or(eq(userId,current), eq(isShared,true))) + 403 on miss; CR-01 deterministic orderBy(...desc).limit(1) closes shared-account IDOR (events.ts:251-260,342-368,474-497) closed
T-03-07 Info Disclosure sync-status leaks another member's row mitigate WHERE and(eq(userId,current), eq(uid)) (events.ts:547) closed
T-03-08 Tampering XSS/oversized payload via title/location/description mitigate zod bounds (title 255, loc/desc 2000); IN-03 worker re-validates outbox payload + hard-fails invalid rows before VEVENT build (events.ts:100-109, outboxWorker.ts:70-82,231-234,323-326) closed
T-03-09 Tampering SQLi via uid/calendarUrl mitigate Drizzle parameterized queries incl. bound sql\`` params; no string interpolation (events.ts:181-198) closed
T-03-10 Spoofing client-supplied etag bypass mitigate etag read server-side at enqueue; client never supplies it (events.ts:407,432,507) closed
T-03-11a EoP writable-calendars surfaces another member's personal calendar mitigate WHERE or(eq(userId,current), eq(isShared,true)) (events.ts:600) closed
T-03-11b Repudiation silent last-write-wins on concurrent edit mitigate 412→conflict:true→mark failed, no overwrite + targeted resync; CR-02 fresh-etag re-read joins calendars on (userId,url)+limit(1) (outboxWorker.ts:265-279,362-370,543-548) closed
T-03-12 DoS poison row retrying forever mitigate MAX_ATTEMPTS=5 + bounded backoff + dead-letter (outboxWorker.ts:40,46,578-587) closed
T-03-13 Info Disclosure logging decrypted app password mitigate Decrypt local-only; per-item catches log err.message only (outboxWorker.ts:127,174-177,608-611; poller.ts:70-74) closed
T-03-14 Tampering partial-failure data loss on edit-as-move mitigate create-before-delete + durable sibling-status gate + create-fail skips delete; CR-01 _preservedRrule re-applied via validated passthrough (outboxWorker.ts:336-353,462-524) closed
T-03-15 Tampering XSS via form title/location/description mitigate All fields plain-text JSX children; no dangerouslySetInnerHTML in apps/pwa/src (EventForm.tsx:557,591,729,752,798) closed
T-03-16 EoP client offers non-writable calendar in picker mitigate Picker only from authoritative fetchWritableCalendars; server re-enforces (client.ts:273-284, EventForm.tsx:182-187) closed
T-03-17 Tampering accidental/irreversible delete mitigate Mandatory two-tap dialog; no single-tap; no "don't ask again" (DeleteConfirmationDialog.tsx:78-81) closed
T-03-18 Repudiation silent data loss on failed delete sync mitigate failed/dead toast persists until dismiss; invalidates ['events'] so server refetch restores (SyncStateToast.tsx:59,201-222) closed
T-03-19 Info Disclosure another member's sync-status in toast mitigate Toast queries own lastSyncedUid; server scopes by member (SyncStateToast.tsx:41, events.ts:547) closed
T-03-20 Spoofing SW caches shell for /callback, breaks OIDC mitigate navigateFallbackDenylist: [/^\/callback/, /^\/api\//, /^\/health/] (vite.config.ts:16-20) closed
T-03-21 Tampering SW caches authenticated API responses mitigate runtimeCaching: [] (vite.config.ts:22) closed
T-03-22 Info Disclosure manifest/icons leak secrets accept Static public assets only; no secrets in manifest closed
T-03-23 Spoofing dev-auth bypass active in live deploy mitigate First guard NODE_ENV==='production'→no-op; prod mounts OIDC unconditionally; WR-04 moved worker startup into isMainModule() gate without altering middleware mount order (devBypass.ts:61, index.ts:38,46-48,104-114) closed
T-03-24 Info Disclosure OIDC redirect_uri mismatch leaks codes mitigate OIDC_AUTH_EXTERNAL_URL MANDATORY = public URL (middleware.ts:12, index.ts:44-45); deployment-config responsibility, no code gap closed
T-03-25 Tampering SW intercepts /callback in live build mitigate Same denylist verified vs production build (vite.config.ts:16-20); Gate 2 row 4 confirmed standalone login 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-03-01 T-03-02 Outbox payload/etag are server-side-only rows, never exposed to the frontend; payload is the member's own VEVENT Lucas Berger 2026-06-09
AR-03-02 T-03-05 write.ts is a low-level CalDAV primitive with no auth context; ownership is enforced one layer up at the route (T-03-06) Lucas Berger 2026-06-09
AR-03-03 T-03-22 PWA manifest and icons are static public assets; contain no secrets Lucas Berger 2026-06-09

Security Audit Trail

Audit Date Threats Total Closed Open Run By
2026-06-09 25 25 0 gsd-security-auditor (opus)

Notes: Verified against the post code-review-fix implementation. The five fix areas (CR-01 member-scoped lookups, CR-01 move-path RRULE forwarding, CR-02 fresh-etag re-read, IN-03 worker payload re-validation, WR-04 worker-startup gate) were each re-verified as present and non-regressing. T-03-24 is a deployment-config control (no code gap). No unregistered threat flags surfaced across the Phase 03 summaries.


Sign-Off

  • All threats have a disposition (mitigate / accept / transfer)
  • Accepted risks documented in Accepted Risks Log
  • threats_open: 0 confirmed
  • status: verified set in frontmatter

Approval: verified 2026-06-09