From 2785bebd3292c5e352b2515eeb3c5203ae4e3d1c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 18 Oct 2026 18:04:41 +0200 Subject: [PATCH] docs(phase-19.1): add security threat verification --- .../19.1-chat-satellites/19.1-SECURITY.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .planning/phases/19.1-chat-satellites/19.1-SECURITY.md diff --git a/.planning/phases/19.1-chat-satellites/19.1-SECURITY.md b/.planning/phases/19.1-chat-satellites/19.1-SECURITY.md new file mode 100644 index 00000000..b3b4a6fd --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-SECURITY.md @@ -0,0 +1,96 @@ +--- +phase: "19.1" +slug: "chat-satellites" +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-10-18" +--- + +# Phase 19.1 — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. + +Implementation lives in the sibling repos `sm-userfriends-plugin`, `sm-forum-plugin`, `sm-chatbots-plugin`, `sm-herald-plugin`, `sm-chatvideo-plugin`; framework pieces are `modules/breakwater`, `modules/fetchguard` and `scripts/check-phase19.1.sh`. Paths below are relative to the meta repo. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| JWT actor → `/_userfriends/api/v1` | untrusted body and path ids | friendship and group ids | +| `user.Deleting` → userfriends GDPR cascade | deleted user must not leave owned groups orphaned | group ownership, member and ban rows | +| JWT actor → `/_forum/api/v1` | untrusted markdown and context ids | post content (HTML sanitizer boundary) | +| `ForumAccessResolver` / `BotAuthResolver` | host-supplied allow; default deny-all | access decisions | +| JWT actor → chatbots `POST /dispatch` | untrusted input string; invoker never from body | command input | +| Client → herald subscription | user-supplied push endpoint URL | URL (SSRF) | +| Herald send job → push service | outbound HTTPS | push payloads | +| Operator → `vapid-generate` | VAPID private key | secret (stdout only) | +| JWT actor → `/_chatvideo/api/v1` | conversation id, join_code | session and call-link ids | +| Token mint → client | LiveKit HS256 join token is a capability grant | signed token | +| Authorizer → realtime subscribe | client-chosen `video:session:{id}` | channel name | +| Golden fixtures → repo | captured payloads | must not include secrets | +| Phase gate → CI | a skip must not count as green | test results | + +--- + +## Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status | +|-----------|----------|-----------|----------|-------------|------------|--------| +| T-19.1-01 | Information disclosure | `sm-userfriends-plugin/api/friends.go` | high | mitigate | Actor from `bouncer.User` (`api/auth.go:18-24`); accept/reject/cancel/unfriend/list scoped to actor in `classes/friendship_service.go`, `ErrNotFound` → 404 (`api/errors.go:54-55`); `community-join` 10/min (`routes.go:60-63,85`) | closed | +| T-19.1-08 | Information disclosure | `sm-userfriends-plugin/boot.go` onUserDeleting | medium | mitigate | `boot.go:79-88` on `ev.Tx`; heir/hard-delete, member and ban removal in `classes/group_service.go:729-766`; `TestUserDeletingHeir` (required postgres test in gate) | closed | +| T-19.1-04 | Tampering | `sm-forum-plugin/classes/forum_service.go` write path | high | mitigate | `breakwater.Render` on create/reply/edit/preview (`forum_service.go:281,330`, `lifecycle.go:232,387`); allow-list policy and forced `Purify` (`breakwater.go:66-100`); stored HTML served as-is (`api/json.go:132-134`); XSS corpus in `TestForumSemanticHTML`, `TestHTTPTracer`, breakwater tests | closed | +| T-19.1-10 | Elevation of privilege | `sm-forum-plugin/classes/deny_all.go` | high | mitigate | DenyAll default on nil/getter/setter (`forum_service.go:82-127`); Boot Warn (`boot.go:59-61`); context-scoped lookups; forbidden/not-found → 404; `TestAccessResolverDefault` | closed | +| T-19.1-07 | Elevation of privilege | `sm-chatbots-plugin/api/commands.go` | high | mitigate | Identity only from `bouncer.User` (`api/auth.go`); `DispatchBody` carries only `Input`; spoofed `user_id` test in `http_tracer_test.go:110` | closed | +| T-19.1-11 | Elevation of privilege | `sm-chatbots-plugin/classes/deny_all.go` | high | mitigate | DenyAll default, getter/setter fallback (`plugin.go:83-100`), Boot Warn (`boot.go:54-59`); `TestBotAuthResolverDefault` | closed | +| T-19.1-02 | Elevation of privilege | `sm-herald-plugin/classes/subscription_service.go`, `sender.go` | high | mitigate | `ValidateEndpoint` at subscribe and patch (`subscription_service.go:39-110`); `fetchguard.PublicOnlyMode` client at send (`sender.go:72-79,205`); `TestSubscribeEndpoint`; no `EndpointGuard` seam | closed | +| T-19.1-06 | Information disclosure | `sm-herald-plugin/console/vapid_generate.go` | high | mitigate | Key pair printed via command output only (`vapid_generate.go:21-27`); no key material in slog calls; `GET vapid-public-key` returns `publicKey` only (`api/push.go:113-125`) | closed | +| T-19.1-03 | Elevation of privilege | `sm-chatvideo-plugin/classes/livekit.go` token mint | high | mitigate | `sub` = user id, HS256, `ErrNotConfigured` on empty secret (`livekit.go:49-83`); secret from server config only; mint only after `requireParticipant`; `TestLiveKitTokenClaims` | closed | +| T-19.1-05 | Elevation of privilege | `sm-chatvideo-plugin/classes/authorizer.go` | high | mitigate | Strict `video:session:{digits}` parse (`authorizer.go:84-103`); participant check (`:54-79`); `prs` capability only; `TestVideoAuthorizer` | closed | +| T-19.1-09 | Information disclosure | `sm-chatvideo-plugin/api/call_links.go` | medium | mitigate | `call-link` 10/min (`routes.go:38-40,63`); uniform `ErrNotFound` → 404 (`video_service.go:266-282`, `api/errors.go:37-38`) | closed | +| T-19.1-12 | Information disclosure | `testdata/goldens/` (five plugins) | medium | mitigate | Fixtures verified clean (no VAPID private key, LiveKit secret, JWT or key-shaped strings). The planned grep gate is not in `check-phase19.1.sh`; follow-up adds it with a `--self-test` plant | open — below high threshold (non-blocking) | +| T-19.1-SC | Tampering | `go.mod` of five plugins, `scripts/check-phase19.1.sh` | high | mitigate | Only decision-noted pins (webpush-go v1.4.0, livekit server-sdk-go/v2 v2.19.0, bluemonday indirect via framework); userfriends `sm-chat-plugin` require accepted as override in VERIFICATION; gate fail-closed (`check-phase19.1.sh:100-145`) with `--self-test` (`:199-251`) | 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)* + +### Unregistered findings (from 19.1-REVIEW.md, no threat ID) + +Security-adjacent review findings still `open` in `19.1-REVIEW-DISPOSITION.md`. Not blocking; listed so they are not lost. + +| Finding | Location | Note | +|---------|----------|------| +| WR-01 | `sm-chatvideo-plugin/classes/livekit.go:125-146` | "Operator URL → LiveKit: TrustedMode only" boundary not enforced; the fetchguard client is built but the SDK uses its own HTTP client. Operator-configured URL, low risk. | +| WR-04 | `sm-chatbots-plugin/classes/dispatcher.go:30-42` | `POST /dispatch` never calls `IsAvailable`; a host-registered restricted command is invocable by any JWT user. Latent (only `/help` registered). | +| WR-05 | `sm-chatvideo-plugin/updates/202610180001_create_golem15_chatvideo_baseline.go:27-28` | No `user.Deleting` listener; `ON DELETE CASCADE` drops sessions incl. other users' participant rows, LiveKit room not deleted, issued tokens valid until `token_ttl`. | +| IN-02 | `sm-chatvideo-plugin/classes/livekit.go` | `screen_share` config not applied; tokens always grant `canPublish`. | +| IN-04 | userfriends `group_invites.created_by` | No FK, not cleared on user delete. | + +--- + +## Accepted Risks Log + +No accepted risks. + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| 2026-10-18 | 13 | 12 | 1 (non-blocking, medium) | msd-security-auditor | + +--- + +## 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 + +**Approval:** verified 2026-10-18