diff --git a/.planning/phases/20-demo-instance-with-every-shared-plugin/20-SECURITY.md b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-SECURITY.md new file mode 100644 index 00000000..66ddf280 --- /dev/null +++ b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-SECURITY.md @@ -0,0 +1,153 @@ +--- +phase: "20" +slug: "demo-instance-with-every-shared-plugin" +status: verified +# threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate) +threats_open: 0 +asvs_level: 1 +created: "2026-11-05" +--- + +# Phase 20 — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. Static audit of repository contents (no SSH, no ls-remote, no host contact). Audited: sm-demo-app da03c06 with plugin submodules on their tags (chat v0.1.5, forum v0.1.2, moderation v0.1.1, chatvideo/herald/userfriends v0.1.1, chatchannels/chatbots v0.1.0); vue-demo-app pins vue-chat-layer v0.1.1, vue-chatvideo-layer v0.1.1, vue-forum-layer v0.1.0, vue-userfriends-layer v0.1.0. Paths are relative to `../` (the meta repo). One medium threat (T-20-32) is open and non-blocking. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| chat/forum HTTP clients → plugin APIs | message and post bodies untrusted; the saving seam runs before storage | message content, thread title, Markdown, JWT (high) | +| host listeners → plugin services | the host decides refusals; listener errors must not leak internals | saving events, Rejection, errors (medium) | +| admin settings → moderation filter | admins edit lists every message is matched against | banned word/link entries (medium) | +| build machine → git.golem15.com / proxies / origin | tags become immutable inputs of every consumer | tags, pushes, go.sum, pnpm lock (medium) | +| browser → /_demo, /_chat, /_chatchannels, /_chatbots, /_chatvideo, /_forum, /_userfriends | every request untrusted; actor only from the JWT | JWT, JSON, uploads ≤5 MiB (high) | +| Centrifugo subscribe proxy → Go / browser → Centrifugo | client channel names, authorized server-side | channel names, connection tokens, X-Centrifugo-Secret (high) | +| seed/reset commands → DB | trusted operator commands; persona accounts must stay unusable | persona rows, random bcrypt hashes (medium) | +| Go → LiveKit RoomService (loopback) / browser → LiveKit | admin calls signed with the API secret; WebRTC with short-lived join token | admin JWT, join token (TTL 900), media, ICE (high) | +| members' content → other members' browsers | untrusted text, links, cards, content_html, names | rendered content (high) | +| realtime events → incoming-call prompt | server payloads about other users | session_id, conversation_id, caller_name (low) | +| npm / git → layers | third-party (livekit-client) and own layers | packages, lockfile integrity (medium) | +| internet → nginx (Cloudflare) / rome public 7891/tcp, 7892/udp | six API prefixes plus /livekit/; WebRTC media straight to LiveKit | HTTP, WS, SRTP, ICE (high) | +| build machine → rome (SSH) | secrets generated on rome; releases and rsync | binaries, configs (high) | +| demo services ↔ WavePath on the same host | must stay isolated | ports, supervisor programs, LiveKit config (high) | +| test harnesses / browser checks → local and live demo | throwaway accounts, visitor creds, live JWT files | JWTs (0600 files), probe content (medium) | +| build machine → summercms.io | journal token publishing changelog posts | g15_ token (high) | + +--- + +## Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status | +|-----------|----------|-----------|----------|-------------|------------|--------| +| T-20-01 | Tampering | chat write paths that skip the seam | high | mitigate | MessageSaving fired on the only message insert and content update (sm-chat-plugin classes/service.go:314-337, writes.go:267-276); forum equivalents; TestMessageSavingTracer/Rejects. Chatbots announce path inserts without the seam but is unreachable on the demo (Ephemeral commands) | closed | +| T-20-02 | Information disclosure | listener errors in API bodies | medium | mitigate | only Rejection → 422, everything else opaque 500 (chat/forum api/errors.go) on every write handler | closed | +| T-20-03 | Denial of service | filter matching cost | medium | mitigate | QuoteMeta + RE2, recompile only on list change; 500 entries / 200 chars enforced in BeforeValidate | closed | +| T-20-04 | Elevation of privilege | moderation settings screen | medium | mitigate | `PermissionManageSettings`, role `developer`, enforced by cabana | closed | +| T-20-05 | Tampering | one-way module tags | medium | mitigate | all family tags annotated, on origin/main, monotonic | closed | +| T-20-06 | Elevation of privilege | channel creation by members | high | mitigate | `CanManageContext` always false (plugins/demo/classes/chat_context.go:272-274); tests refuse member and moderator | closed | +| T-20-07 | Information disclosure | subscribing to other conversations | high | mitigate | chat/user/video authorizers admit only own or participant channels; all namespaces via subscribe proxy. Residual IN-02 | closed | +| T-20-08 | Spoofing | persona accounts | medium | mitigate | `personas.demo.invalid` emails, random bcrypt hashes never printed; reset reseeds | closed | +| T-20-09 | Elevation of privilege | moderator rights | medium | mitigate | group conversations only, member + moderator group, banned users excluded, live membership required; group seeded empty | closed | +| T-20-10 | Tampering | spam/abuse in chat and forum | high | mitigate | moderation listeners on both seams; rate buckets (chat 120/min, DM 10/min, forum, video-initiate 5/min); Turnstile `closed`; manual demo:reset (schedule off in production). Residual WR-03 | closed | +| T-20-11 | Denial of service | realtime token minting | low | mitigate | JWT + membership guard, 120/min per user | closed | +| T-20-12 | Information disclosure | LiveKit and VAPID secrets (local) | high | mitigate | dev.sh umask 077, names only echoed; empty examples; CHANGE_ME placeholders enforced by check-deploy.sh:391-399 | closed | +| T-20-13 | Denial of service | stale chat-family rows after reset | low | mitigate | reset lists chat-family tables before users; reset tests | closed | +| T-20-14 | Tampering | stored XSS (text, names, URLs) | high | mitigate | vue-chat-layer v-html only through the escaping renderer; safeUrl, rel noopener noreferrer nofollow; linkify on escaped text with honestHttpUrl | closed | +| T-20-15 | Information disclosure | JWT handling in the layer | medium | mitigate | token from auth.token() as Bearer to same-origin bases; no web storage or cookie use | closed | +| T-20-16 | Information disclosure | attachment downloads | medium | mitigate | bearer download, object URL; server checks CanView | closed | +| T-20-17 | Spoofing | slash command answers | low | mitigate | `/` text dispatched, never posted; local ephemeral row marked "only you" | closed | +| T-20-18 | Tampering | layer supply chain | medium | mitigate | exact pins; layers pinned by tag and locked to commits | closed | +| T-20-19 | Denial of service | realtime connections | low | mitigate | single Centrifuge client; bounded polling | closed | +| T-20-20 | Tampering | forum stored XSS | high | mitigate | v-html only on server-rendered content_html; `breakwater.Render` (bluemonday) on write; server-side preview | closed | +| T-20-21 | Spoofing | call joins | high | mitigate | requireParticipant on start/join, token TTL 900, max 4 participants; layer never builds tokens | closed | +| T-20-22 | Information disclosure | camera and microphone | medium | mitigate | media only after handoff/click; disconnect stops tracks; leave on pagehide/unmount | closed | +| T-20-23 | Tampering | incoming-call payloads | low | mitigate | text binding for caller name; positive integer ids only | closed | +| T-20-24 | Denial of service | call spam | medium | mitigate | video-initiate 5/min, max_duration 900, reconcile job; DM-only slot in the UI. Residual IN-01 | closed | +| T-20-25 | Information disclosure | friend enumeration | low | mitigate | generic 422 shown as is; directory = chat picker set | closed | +| T-20-26 | Information disclosure | VAPID private key | high | mitigate | never printed; empty example; no `/_herald` route in Nuxt or nginx, enforced by check-deploy.sh:405-409 | closed | +| T-20-27 | Elevation of privilege | LiveKit co-tenancy with WavePath | high | mitigate | own supervisor program, config, user, ports, key pair; WavePath config untouched (DEPLOY.md:355-410). Host state live-only | closed | +| T-20-28 | Information disclosure | LiveKit, VAPID, Centrifugo secrets (rome) | high | mitigate | generated on rome, `install -m 0640 root:summercms-demo`, written via stdin, unset; placeholders enforced. File modes live-only | closed | +| T-20-29 | Denial of service | open media ports | medium | mitigate | only 7891/tcp and 7892/udp via Vuurmuur; participant and timeout limits. Firewall live-only | closed | +| T-20-30 | Information disclosure | origin IP in ICE candidates | low | accept | AR-20-01 | closed | +| T-20-31 | Elevation of privilege | unintended Go routes | medium | mitigate | six chat-family locations + `/livekit/`, no /_herald, `/backend` 404; check-deploy.sh. Residual IN-06 | closed | +| T-20-32 | Tampering | firewall drift | medium | mitigate | Vuurmuur change and removal documented (DEPLOY.md:467-490), but **no dated rules.conf backup**; DEPLOY.md:475-476 points to a "Probed on rome on 2026-10-31" section (:94-102) that lists no rules | open — below high threshold (non-blocking) | +| T-20-33 | Denial of service | a release breaking the live demo | medium | mitigate | `.prev` binary and site kept, rollback documented, check-live after release; clean-tree builds | closed | +| T-20-34 | Spoofing | signalling WebSocket | low | mitigate | tokens only after requireParticipant (TTL 900); `/rtc/validate` 401 without token | closed | +| T-20-35 | Information disclosure | credentials in tests and check output | medium | mitigate | visitor password read from `.env`, never printed; failures name keys only. Residual IN-03 (argv) | closed | +| T-20-36 | Tampering | regressions behind skipped tests | medium | mitigate | check-all.sh runs suites without `-short`; container start failure fails tests; PLAN verifies count `--- PASS` lines | closed | +| T-20-37 | Denial of service | throwaway accounts on the live demo | low | accept | AR-20-02 (rationale corrected) | closed | +| T-20-38 | Spoofing | banned-link matching | high | mitigate | sm-moderation-plugin normalize.go canonical links (dots, percent-decode, last `@`, port, IDNA, path); TestMatchBypassLinks | closed | +| T-20-39 | Tampering | banned-word matching | high | mitigate | entity decode, NFKC, Cf/Cc drop, space folding, diacritics, markup strip; two passes; TestMatchBypassWords | closed | +| T-20-40 | Tampering | forum Markdown rendered after the check | high | mitigate | PostSaving.HTML carries rendered HTML; demo runs CheckHTML on it; forum, moderation and demo tests | closed | +| T-20-41 | Denial of service | normalising and matching large texts | low | accept | AR-20-03 | closed | +| T-20-42 | Information disclosure | useCall: media after leave | critical | mitigate | per-attempt `stale()` checks at every await; stale branch drops the Room; tests cover seven moments | closed | +| T-20-43 | Elevation of privilege | /call/{id} opened from a link | high | mitigate | confirmation unless an in-memory 60 s single-use handoff exists | closed | +| T-20-44 | Information disclosure | chat store, socket, call prompt after logout | high | mitigate | isolated sign-out steps; reset, disconnect, dismiss wired; communityAuth tests; check-chat.cjs requires `/ws` closed in 5 s | closed | +| T-20-45 | Denial of service | empty ringing session after a failed connect | medium | mitigate | closeFailed ends sessions with ≤1 participant; tests | closed | +| T-20-46 | Spoofing | deceptive hosts in anchors and cards | high | mitigate | `honestHttpUrl` (vue-chat-layer urlGuard.ts:28-48) on every link path; tests | closed | +| T-20-47 | Tampering | release tags | high | mitigate | annotated, on origin/main, monotonic; ls-remote and fast-forward recorded in 20-09-SUMMARY | closed | +| T-20-48 | Information disclosure | live member JWTs | high | mitigate | mode 600/400 required, header via `curl -K -` stdin; delete afterwards documented and recorded. Deletion live-only | closed | +| T-20-49 | Tampering | probe content on the live demo | medium | mitigate | scope limited to own DM and seeded thread; stored probes deleted and reported; controls refused first | closed | +| T-20-50 | Information disclosure | journal token | high | mitigate | changelog-post.py own token file, refuses readable-by-others, never printed | closed | +| T-20-51 | Denial of service | the shared host during a release | medium | mitigate | restarts only the demo programs; rollback; pids recorded. Pid check live-only | closed | +| T-20-52 | Tampering | temporary integration leaking into a release | medium | mitigate | clean-tree builds, GOWORK=off, frozen lockfile; tag-only pins; no go.mod replace | closed | +| T-20-SC | Tampering | npm / Go installs (plans 01-09) | high | mitigate | every new module matches an already-vetted version (sitemap or framework); livekit-client 2.23.0 exact with lock integrity and human checkpoint; `protobufjs: false`; later plans change only golem15 modules and layer tags | closed | + +*Status: open · closed · open — below high threshold (non-blocking)* +*Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open* +*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)* + +--- + +## Accepted Risks Log + +| Risk ID | Threat Ref | Rationale | Accepted By | Date | +|---------|------------|-----------|-------------|------| +| AR-20-01 | T-20-30 | rome already exposes services on its public address; the ICE candidates show it (DEPLOY.md:367-369). | 20-05 PLAN | 2026-11-05 | +| AR-20-02 | T-20-37 | check-live registers no accounts (its token-less register expects 422). **Corrected:** the plan said the reset job removes the human UAT account, but the reset schedule is off in production (DEPLOY.md:945-946, 1006-1009), so UAT accounts stay until someone runs `demo:reset --force` by hand. | 20-09 PLAN (corrected by audit) | 2026-11-05 | +| AR-20-03 | T-20-41 | RE2 is linear, lists are bounded by max_entries and max_entry_length, and the work is two passes over a message the plugins already cap in size. | 20-07 PLAN | 2026-11-05 | + +*Accepted risks do not resurface in future audit runs.* + +--- + +## Open Below Threshold + +| Threat | Severity | Gap | To close (any one) | +|--------|----------|-----|--------------------| +| T-20-32 | medium | no dated `rules.conf` backup before the Vuurmuur change; DEPLOY.md's substitute reference points at a section without the rules | take a dated `rules.conf.bak-` on rome, record the 15 pre-change RULE lines in DEPLOY.md's "Probed on rome" section, or record an accepted risk | + +--- + +## Unregistered Flags and Residual Risk (non-blocking) + +- **Unregistered (20-02):** `GET /_demo/api/v1/chat/members` (sm-demo-app/plugins/demo/chat_api.go) lists every member's name, surname and handle to any member; IN-09 adds that the GET writes handles and builds an unbounded `IN` list (500 above 65535 members). Todo `demo-chat-members-read-only.md`. +- Deferred review items, each with a pending todo: WR-02 (any member can pin/unpin in public channels), WR-03 (profile names, friend-group names and attachment filenames are not moderated), WR-06 (forum/friends clients lack 401 refresh), IN-01 (no server-side DM-only call rule), IN-02 (video join token survives a ban up to 900 s), IN-03 (member JWTs and — found by this audit — the visitor password at check-local.sh:333 in curl argv), IN-04, IN-05, IN-06 (nginx `/livekit/` exposes LiveKit's whole HTTP surface incl. admin-JWT-protected RoomService), IN-07, IN-08 (VAPID runbook without `pipefail`). +- Untracked: the 19.4.1 Centrifugo secrets runbook (DEPLOY.md:327-328) passes secrets as `sed` arguments on rome (not echoed; noted in 20-05 deviation 3, not fixed). + +### Host-only checks (not verifiable from the repos) + +WavePath LiveKit untouched and pid unchanged (T-20-27, T-20-51); file modes of `livekit.yaml`, `.env`, `centrifugo.json` (T-20-28); the applied Vuurmuur rule (T-20-29, T-20-32); live JWT files deleted (T-20-48); `RESET__ENABLED` count 0. + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| 2026-11-05 | 53 | 52 | 1 (medium, non-blocking) | msd-security-auditor (ASVS L1, block_on high; static, repo contents only) | + +--- + +## 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 +- [ ] T-20-32 closed (backup or recorded rules) or accepted +- [ ] Host-only checks confirmed on rome + +**Approval:** verified 2026-11-05 (ASVS L1; T-20-32 open below threshold)