Files
OwnCord/docs/audit-2026-04-07.md
T

450 lines
29 KiB
Markdown
Raw Normal View History

2026-04-07 10:13:47 +02:00
# OwnCord — Comprehensive Project Audit
**Date:** 2026-04-07
**Branch:** claude/plan-phases-b-c-bGpoS
**Audited by:** 6 parallel Claude agents across 8 dimensions
---
## Finding closure status (maintained; last updated 2026-07-20)
2026-07-18 11:17:29 +02:00
Every CRITICAL/HIGH below must end with a closing commit link or an explicit
mitigation before the beta gate. Standing rule: any plugin CRITICAL still
OPEN at the beta gate → plugins ship default-disabled.
**Rule status 2026-07-20:** finding #5 is closed as *accepted residual risk*,
not fixed, so the rule fires — plugins ship default-disabled at beta.
Verified in code: `config.DefaultConfig()` sets `Plugins.Enabled: false` and
`Plugins.HTTPAllowlist: []string{}` (`Server/config/config.go:206-212`), and
`hostAllowed` denies every host against an empty allowlist
(`Server/plugin/host_http.go:140-158`, pinned by
`TestEmptyAllowlistDeniesEveryHost`).
**Structural mitigation covering #2, #4 and #5:** no host imports are wired
into the wazero runtime. `activateWithRuntime` instantiates guest modules with
WASI preview-1 only (`Server/plugin/sandbox_wazero.go:69-124`), and
`HTTPDo` / `Storage*` have no callers outside the `plugin` package's own
tests. `EventSink.Dispatch` is the exception — `Server/ws/hub.go:1034` calls
it on every broadcast when plugins are enabled — but its loop body is inert
and nothing outside tests calls `EventSink.Subscribe`, so it iterates over an
empty subscriber set and reaches no guest code (see #4). The only
guest-reachable entry points today are `command_dispatch` (via the WS
`chat_command` handler) and `list_commands` at activation. Wiring those host
imports is what makes #5 exploitable at all and is the point at which #4's
rate limit must exist.
2026-07-18 11:17:29 +02:00
| # | Sev | Finding | Status |
|---|-----|---------|--------|
| 1 | CRITICAL | Plugin `invokeCommand` has no timeout | **CLOSED 2026-07-20** — verified in code. Every guest call (`allocate` / `command_dispatch` / `deallocate`) runs under a per-invocation deadline: `budgetMs` = manifest `resources.cpu_budget_ms``plugins.cpu_budget_ms` → hard 100 ms floor, applied via `context.WithTimeout` (`Server/plugin/sandbox_wazero.go:257-268`). The runtime is built `WithCloseOnContextDone(true)` (`sandbox_wazero.go:69-73`) so an expired deadline interrupts a runaway guest (`for {}`), and `releaseClosedModule` (`sandbox_wazero.go:326-338`) drops the closed module so the next dispatch re-instantiates lazily instead of bricking the plugin (regression W1-1). Landed in PR #1182 (`0f58ddd` budget, `2111976` W1-1). Pinned by `TestWazeroCPUBudgetOverrunDoesNotBrickPlugin` (`sandbox_wazero_test.go:271`) |
| 2 | CRITICAL | Plugin storage has no per-plugin key isolation | **CLOSED 2026-07-20** — the finding's premise does not hold against the code. Isolation is structural, not a check that can be skipped: every `Storage*` call passes the caller's `Instance.ID` as the namespace and exposes no parameter by which a caller — let alone a guest module — could name another plugin's namespace (`Server/plugin/host_storage.go:26-73`), and `plugin_kv PRIMARY KEY (plugin_id, key)` (`Server/migrations/015_plugins.sql:13-18`) makes the same split the storage layout. Every query filters on `plugin_id` (`Server/db/plugin_queries.go:87-135`). This PR adds `TestStorageKeysIsolatedPerPlugin` pinning it (same key from two plugins does not collide; scan/delete do not cross namespaces) plus the missing key-size cap the file's doc comment already promised |
| 3 | CRITICAL | Plugin per-command ACL missing (auto-registration) | **CLOSED 2026-07-20 (this PR)** — the manifest is now the per-command ACL. `plugin.json` gains a `commands` block; `RegisterCommand` refuses any name the manifest did not declare (`Server/plugin/host_commands.go:31-45`, `ErrCommandNotDeclared`), which is the single choke point both `list_commands` auto-registration (`sandbox_wazero.go:146-153`) and direct registration route through. A guest can therefore no longer widen its own command surface, and an admin can see the full command list before enabling. Declared names are validated to the dispatcher's canonical form, deduplicated, and capped at 64 (`manifest.go:207-233`). Cross-plugin hijack was already refused and stays refused. Pinned by `TestRegisterCommandRequiresManifestDeclaration` + `TestManifestCommandsValidation` |
| 4 | CRITICAL | No rate limit on event delivery to plugins | **CLOSED 2026-07-20 (no guest code on the event path)** — there is no guest delivery to rate-limit. Note what *is* wired, so this is not mistaken for an absent call site: `EventSink.Dispatch` has exactly one caller outside the `plugin` package's tests — `Server/ws/hub.go:1034`, invoked on **every** broadcast message whenever an operator enables plugins (`Server/api/router.go:134-139` sets `h.pluginSink` when the registry is non-nil), on the hub's broadcast goroutine while `seqMu` is held. What makes the finding unreachable is one level down: `Dispatch`'s loop body invokes no guest code in either build (it touches no `inst.module`), and no production code calls `EventSink.Subscribe` — only tests — so `subs` is empty and the loop never iterates. A plugin cannot slow the hub by handling events slowly because no plugin ever handles one. Recorded as a gate rather than left silent: the SECURITY GATE comment on `Server/plugin/host_events.go` requires the per-plugin rate limit, the `invokeCommand` CPU deadline, and off-hub-goroutine delivery to land *in the same change* that wires guest delivery — and flags that the hot call site already exists, so wiring is a one-line `Subscribe` away, not a new integration. `TestEventDeliveryHasNoGuestPath` fails if delivery appears without that review |
| 5 | CRITICAL | Plugin HTTP capability allows data exfiltration to allowlisted hosts | **OPEN — accepted residual risk (2026-07-20)**. Not fixable by hardening: an allowlisted host is by definition a permitted destination, so a plugin holding `http` can POST anything it can read to it. Closing it properly needs egress content policy (per-plugin request/response body inspection, byte budgets, per-plugin allowlists instead of one server-wide list) — a plugin-runtime redesign, not a patch. Standing mitigations, all verified in code: (a) `plugins.enabled` defaults false; (b) `plugins.http_allowlist` defaults empty and an empty allowlist denies every host, so the capability is inert until an operator names a destination; (c) the manifest must declare `http`, which is visible to the admin before enabling; (d) no host import is wired, so guest code cannot call `HTTPDo` at all today; (e) SSRF hardening (allowlist dot-boundary matching, guarded dial that vets every resolved IP before connecting, redirect re-checks, 5 MiB response cap) confines reach to public allowlisted hosts. Residual risk accepted for alpha/beta: an operator who both enables plugins and allowlists a host trusts the plugins they install with data those plugins can read |
| 6 | HIGH | `Server/store/` untested | SUPERSEDED — `store/` was removed 2026-07-19 (single data layer, A-2026-07-05); its behavior lives in `db/` + sqlc (`db/dbgen/`), tested against in-memory SQLite |
| 7 | HIGH | Client `src/lib`/`src/stores` <10% unit coverage | CLOSED since audit — large vitest suite exists (164 files / ~4.4k tests as of 2026-08-04, ~94% statements); `src/lib` + `src/stores` are also mutation-tested (Stryker) |
| 8 | HIGH | Unpinned critical npm packages | RESOLVED 2026-08-05 — the dependency policy is decided and written down in docs/contributing.md (lockfiles authoritative + npm ci-only installs + weekly Dependabot with deliberate majors + per-PR audit gates); DC-11 was this finding's tracker |
2026-07-18 11:17:29 +02:00
| 9 | MEDIUM | auth_handler bypasses service layer | OPEN — P4 consolidation candidate |
| 10 | MEDIUM | Audit-trail write failures silently ignored | RESOLVED 2026-07-20 — every audit write routes through `db.WriteAudit` (`Server/db/audit.go:37`), which enqueues to the async writer and, on the synchronous fallback, logs failures via `slog.Error` with action/actor/target context. Nothing is silently dropped |
| 11 | MEDIUM | E2E not in CI / no .nvmrc | RESOLVED — better than planned: the full web e2e suite runs on every PR (`client-e2e`, non-blocking pending a flakiness soak) and the `@parity` subset gates merges (`client-e2e-parity`, blocking). Node is pinned by `setup-node` to 20 in CI |
2026-07-18 11:17:29 +02:00
---
2026-04-07 10:13:47 +02:00
## Table of Contents
1. [Architecture](#1-architecture)
2. [Code Quality](#2-code-quality)
3. [Security](#3-security)
4. [Dependencies & Supply Chain](#4-dependencies--supply-chain)
5. [Test Coverage & Quality](#5-test-coverage--quality)
6. [CI/CD & DevEx](#6-cicd--devex)
7. [Observability](#7-observability)
8. [Plugin System Governance](#8-plugin-system-governance)
9. [Prioritized Top-10 Action List](#9-prioritized-top-10-action-list)
---
## 1. Architecture
### Layer Map
| Layer | Location | Responsibility |
|-------|----------|---------------|
| **Client UI** | `Client/tauri-client/src/components/` | SolidJS UI components, settings, voice |
| **Client Pages** | `Client/tauri-client/src/pages/` | MainPage, ConnectPage entry points |
| **Client Stores** | `Client/tauri-client/src/stores/` | Reactive state (auth, channels, messages, voice, dm, ui, roles) |
| **Client Lib** | `Client/tauri-client/src/lib/` | API client, WebSocket client, dispatcher, LiveKit session, theme |
| **Tauri Rust** | `Client/tauri-client/src-tauri/` | WS proxy, credential manager, PTT, update notifications |
| **Server API** | `Server/api/` | REST handlers (auth, channels, messages, DMs, invites, uploads) |
| **Server WS** | `Server/ws/` | Real-time hub, event persistence, command dispatch, LiveKit integration |
| **Server Service** | `Server/service/` | Domain business logic (Message, Channel, Permission, User, DM, Invite, Block, Voice) |
| **Server Store** | `Server/store/` | Data access layer (SQLiteStore, PostgresStore stub, MemStore) |
| **Server DB** | `Server/db/` | SQL schema, migrations, sqlc-generated queries |
| **Server Plugin** | `Server/plugin/` | WASM runtime (Wazero), loader, manifest, registry, capability-scoped host APIs |
| **Server Admin** | `Server/admin/` | Admin UI, backup management, log streaming |
| **Server Auth** | `Server/auth/` | Authentication, session management, rate limiting, TOTP |
### Communication Patterns
| Channel | Usage |
|---------|-------|
| **REST API** | Auth, channel CRUD, file uploads, initial data fetch — uses Tauri HTTP plugin for self-signed cert support |
| **WebSocket** | Real-time events (messages, presence, typing, voice) — single connection per client, envelope format `{type, id, payload}` |
| **Tauri IPC** | `ws_connect/send/disconnect`, credential manager, PTT, update checks — desktop-only features |
| **LiveKit** | HTTP voice token acquisition, reverse-proxy WebSocket signaling at `/livekit/*`, webhook for participant events |
### Dependency Direction
Layering is **healthy and mostly respected**:
-`api/ → service/ → store/` holds
-`ws/` does not import `api/` (no circular deps)
- ✅ Plugin system imports service layer cleanly
- ✅ Auth middleware isolated; not interspersed with handlers
### Anti-patterns
| SEVERITY | File | Finding |
|----------|------|---------|
| MEDIUM | `Server/api/auth_handler.go` | Auth handler queries DB directly, bypassing service layer — inconsistent with `channel_handler.go` pattern |
| MEDIUM | `Client/tauri-client/src/lib/dispatcher.ts` | 84 registered listeners creates implicit coupling; hard to trace data flow |
| MEDIUM | `Client/tauri-client/src/lib/livekitSession.ts` (1710 lines) | Monolith mixing LiveKit SDK, audio pipeline, diagnostics — should decompose |
| MEDIUM | `Client/tauri-client/src/pages/MainPage.ts` (554 lines) | Mixes routing, layout, store subscriptions, cleanup — violates single responsibility |
| MEDIUM | `Server/service/message.go` (715 lines) | Combines send/search/delete/edit/fetch — split into focused services |
| LOW | `Client/tauri-client/src/main.ts` (538 lines) | Entry point handles init, error handlers, theme, health checks, connection orchestration |
| LOW | `Server/api/router.go` (394 lines) | Mixes middleware setup, route registration, and business logic wiring |
| LOW | `Server/admin/` | Admin package operates directly on DB; not using service layer — may diverge from REST/WS semantics |
| LOW | PostgreSQL backend | Fully scaffolded but query methods stubbed; migration path undocumented |
---
## 2. Code Quality
### Go — Error Handling
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| MEDIUM | `Server/admin/logstream.go:398-426` | SSE handler ignores write errors with `_, _ = fmt.Fprintf()` — client disconnections not detectable |
| MEDIUM | `Server/admin/handlers_backup.go:55,139` | `database.LogAudit()` errors silently discarded with `_ =` — audit trail may silently fail |
| MEDIUM | `Server/admin/handlers_users.go` (5 instances) | `LogAudit()` errors discarded — affects compliance/security audit trail |
| LOW | `Server/updater/updater.go` (13 instances) | Excessive `_ =` error suppression; no logging fallback |
| LOW | `Server/db/db.go` (9 instances) | Unchecked errors in setup/teardown paths |
### Go — Interface Design
-**Strong**: Store package has clear interface hierarchy (`Store → MessageStore, ChannelStore, UserStore, SessionStore, RoleStore`) with proper concrete implementations
- ⚠️ **Concern**: Service layer handlers receive concrete `*db.DB` or `*Hub` rather than small interfaces — reduces testability
### Go — Large Files (>800 lines)
| File | Lines |
|------|-------|
| `Server/ws/hub.go` | 919 — mixes client management, event persistence, plugin integration, voice key-holding |
| `Server/store/memstore.go` | 766 |
| `Server/service/message.go` | 715 |
| `Server/store/postgres.go` | 833 — largely stubbed scaffolding |
### Go — Security-Relevant TODOs
| File | TODO |
|------|------|
| `Server/ws/command.go` | Validate attachment URL scheme (require HTTPS) — security gap |
| `Server/ws/registry.go` | Stack trace may contain sensitive function arguments |
| `Server/ws/voice_leave.go` | Propagate context through `livekit.RemoveParticipant()` call |
| `Server/ws/voice_e2ee.go` | Re-check key-holder status inside `sendToUserIfInVoiceChannel` |
| `Server/store/sqlite.go` | Incomplete transaction-scoped store wrapper |
### TypeScript — Type Safety
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| MEDIUM | Multiple components | ~43 event handlers with untyped `(e)` parameters — should be typed as `MouseEvent`, `DragEvent`, etc. |
| LOW | `src/lib/audioPipeline.ts:75` | Single `as any` cast (justified by eslint-disable comment; acceptable) |
### TypeScript — Large Components
| File | Lines | Action |
|------|-------|--------|
| `src/components/settings/AccountTab.ts` | 845 | **Extract**: password-section.ts, totp-section.ts, status-selector.ts |
| `src/components/EmojiPicker.ts` | 665 | Extract emoji data file |
| `src/components/MessageList.ts` | 662 | Extract height-calc module |
| `src/components/MessageInput.ts` | 599 | Reasonable given feature density |
### TypeScript — Error Handling Gaps
| SEVERITY | File | Finding |
|----------|------|---------|
| MEDIUM | `src/components/InviteManager.ts:96,151` | `.then()` chains without `.catch()` — silent rejection |
| MEDIUM | `src/components/message-input/file-upload.ts:57` | `.then()` without catch — corrupt file failures silent |
| MEDIUM | `src/components/message-list/attachments.ts:341` | `void fetchImageAsDataUrl().then()` — image load failures not surfaced |
---
## 3. Security
### Overall Posture: **GOOD** (no critical issues in core app security)
### Secrets & Configuration
-`.env.example` uses placeholder values; file gitignored
- ✅ No hardcoded secrets found in source, Tauri config, or configs
- ✅ LiveKit credentials require 32+ character minimum
### Input Validation
- ✅ Username validation with `auth.ValidateUsername()` + bluemonday HTML sanitization
- ✅ File uploads use `filepath.Base()` to prevent path traversal
- ✅ Plugin uploads: magic-byte ZIP validation, 16 MiB hard cap, decompression bomb protection, symlink rejection
- MEDIUM | `Server/api/auth_handler.go:151` | Username validation doesn't explicitly check for Unicode control characters, RTL overrides, or zero-width chars — potential homograph attack vector
### SQL Injection
- ✅ All standard queries use parameterized statements
-`SearchMessagesInChannels` uses `fmt.Sprintf` only for `?` placeholder structure (not user data) — **safe by design**
- ✅ FTS query sanitization: whitelist-only (letters, digits, spaces, hyphens), max 200 runes
### Authentication & Authorization
-`AuthMiddleware`: Bearer token extraction → hash → session lookup → expiry → ban check → context injection
-`RequirePermission`: Admin bit (0x40000000) bypasses; 403 on insufficient permissions
- ✅ WebSocket in-band auth within 10-second deadline; re-validated every 10 messages
- ✅ Fail-closed: nil role = zero access
- ✅ Permission-filtered reconnect replay (prevents data leakage after permission changes)
### Rate Limiting
- ✅ Per-endpoint configurable rate limits with 429 + Retry-After
- ✅ Login, register, TOTP verification all rate-limited
- ✅ Sensitive endpoints (account deletion, TOTP management) rate-limited
- ✅ IP extraction validates trusted CIDR proxies — prevents rate-limit bypass via spoofed headers
### Tauri Security
- ✅ CSP enforced; `withGlobalTauri: false` (Tauri API not exposed on `window`)
- ✅ Capabilities-based permission model (Tauri v2)
- ✅ No devtools in production build
### WebSocket Security
- ✅ Auth deadline prevents resource exhaustion from slow clients
- ✅ Priority-based send channels (high/normal/low) prevent low-priority spam blocking critical messages
- ✅ Read limit enforced (`wsReadLimitBytes = config.MaxMessageBytes`)
### Observations (not blocking)
| SEVERITY | Area | Finding |
|----------|------|---------|
| MEDIUM | Username validation | Add explicit Unicode control character / zero-width char rejection |
| MEDIUM | TOTP secrets | Confirm AES-256 encryption of stored secrets with per-record salt |
| MEDIUM | LiveKit webhook | Confirm webhook signature validation cannot be bypassed with malformed JWT |
| LOW | Session tokens | Confirm cryptographically secure RNG and per-session salt for token hashing |
---
## 4. Dependencies & Supply Chain
### Overall Posture: **MODERATE RISK** (Go excellent, npm floating)
### Go Modules — 30 direct deps, ALL exact-pinned ✅
Notable: `golang.org/x/crypto v0.49.0`, `github.com/corazawaf/coraza/v3 v3.6.0`, `github.com/tetratelabs/wazero v1.11.0`, `modernc.org/sqlite v1.48.0`
### npm — 13 production deps, ALL floating (^) ⚠️
| Risk | Package | Version |
|------|---------|---------|
| HIGH | `@tauri-apps/plugin-updater` | `^2.10.0` — update mechanism, should pin |
| HIGH | `@tauri-apps/plugin-global-shortcut` | `^2` — any 2.x allowed |
| HIGH | `@tauri-apps/plugin-notification` | `^2` — any 2.x allowed |
| HIGH | `@tauri-apps/plugin-store` | `^2` — any 2.x allowed |
| MEDIUM | `livekit-client` | `^2.18.0` |
| MEDIUM | `solid-js` | `^1.9.3` |
| LOW | `zod` | `^4.3.6` |
**Mitigating factor:** `package-lock.json` (8,832 lines) is committed and locks transitive deps.
### Lockfile Status
| Ecosystem | Status |
|-----------|--------|
| Go | ✅ `go.sum` committed, 455 entries |
| npm (client) | ✅ `package-lock.json` committed, 8,832 lines |
| npm (root) | ✅ `package-lock.json` committed, 492 lines |
### License Compliance
- OwnCord: **AGPLv3**
- All direct dependencies: Apache 2.0 or MIT — **no copyleft incompatibilities**
### Known Vulnerabilities
- No CRITICAL CVEs in direct dependencies as of Feb 2025
- `golang.org/x/crypto v0.49.0` — verify no disclosed CVEs since mid-2024 release
- Run `npm audit` and `govulncheck` periodically
---
## 5. Test Coverage & Quality
### Go — Package Coverage
| Package | Test Files | Status |
|---------|-----------|--------|
| `api/` | 23 | ✅ Heavy behavioral coverage |
| `ws/` | 32 | ✅ Connect/disconnect, commands, auth, deadlock |
| `auth/` | 8 | ✅ Core flows, TOTP, helpers |
| `db/` | 18 | ✅ Operations and migrations |
| `admin/` | 11 | ✅ Good coverage |
| `plugin/` | 6 | ✅ Loading, sandbox, manifest |
| `permissions/` | 3 | ✅ |
| `service/` | 2 | ⚠️ Only message + permission |
| `**store/**` | **0** | ❌ **CRITICAL GAP — data persistence untested** |
| `syncutil/` | 0 | ⚠️ Concurrency utils untested |
| `scripts/` | 0 | Low priority (CLI tool) |
### Go — Test Quality: GOOD
- Behavioral tests using in-memory SQLite (`:memory:`) with full schema
- Table-driven patterns used throughout
- `goleak.VerifyTestMain()` in `auth/` — goroutine leak detection
- Deadlock detection tests via `go test -tags deadlock`
### Go — Critical Coverage Gaps
| SEVERITY | Gap |
|----------|-----|
| HIGH | `Server/store/` — data persistence layer has zero test coverage |
| HIGH | No PostgreSQL integration tests — all use in-memory SQLite; schema drift not caught |
| MEDIUM | `Server/syncutil/` — concurrency utilities untested |
| MEDIUM | Migration safety not validated across versions |
### TypeScript — Test Files
- **1 component unit test**: `src/components/solid/Badge.test.tsx`
- **44 E2E specs**: Playwright (20 browser-mode, 22 native-mode + helpers)
- **1 jsdom smoke test**: `tests/browser/smoke.test.ts`
### TypeScript — E2E Coverage: EXCELLENT
Auth flow, channels, messages, DMs, health/reconnect, UI overlays, voice controls, theme persistence — all covered in 44 Playwright specs.
### TypeScript — Unit Coverage: MINIMAL (<10%)
| SEVERITY | Gap |
|----------|-----|
| HIGH | No tests for `src/lib/` utilities (api.ts, ws.ts, dispatcher.ts, livekitSession.ts) |
| HIGH | No tests for `src/stores/` (messages, channels, voice, auth) |
| MEDIUM | 7 files explicitly excluded from coverage thresholds: `main.ts`, `updater.ts`, `credentials.ts`, etc. |
| MEDIUM | 70% coverage threshold configured in vitest but unit tests too sparse to enforce |
---
## 6. CI/CD & DevEx
### Pipeline Gates
| Job | OS | Gates |
|-----|----|-------|
| `server-build-test` | Windows + Ubuntu | ✅ Build (4 tag variants), Tests (race + deadlock), golangci-lint, govulncheck |
| `client-check` | Windows | ✅ TypeScript check, ESLint, Oxlint, Prettier, Vitest, npm audit, Knip |
| `server-docker-build` | Ubuntu | ✅ Docker build verification |
| `tauri-build` | Windows + Ubuntu + Ubuntu-ARM | ✅ Rust lint, Rust audit, full Tauri build — PR to main only |
### Linting Enforcement
- **Go**: golangci-lint v2.11.3 — hard fail; rules: gocritic, gosec, errcheck, bodyclose, contextcheck, staticcheck
- **TypeScript**: ESLint with `no-floating-promises: error`, `no-unused-vars: error`; Prettier format check; Oxlint
- **Enforcement**: Hard fail for all linting/formatting in CI ✅
### Gaps
| SEVERITY | Finding |
|----------|---------|
| MEDIUM | E2E tests not in main CI — only run on PRs to main, not on merge. Code could merge without E2E gate |
| MEDIUM | No branch protection rules visible in repo config |
| LOW | No `.nvmrc` / `.node-version` file — Node 20 only enforced in CI, not locally |
| LOW | Rust toolchain pinned to `stable` (not specific version) — builds can drift |
| LOW | `Knip` runs with `|| true` (advisory only, not blocking) |
### Build Reproducibility
- ✅ Go: `go 1.25.0` pinned in `go.mod`; `go.sum` committed
- ✅ Node: Version 20 pinned in CI; `npm ci` used (clean install)
- ✅ 4 Go build tag variants tested: default, otel, wazero, otel+wazero
- ✅ Lockfiles current (go.sum: Apr 6, package-lock: Apr 7)
---
## 7. Observability
### Logging: STRONG ✅
- **Library**: `log/slog` (stdlib, Go 1.21) with multi-handler — stdout (INFO+) + ring buffer (DEBUG+)
- **Structured**: All logs use key-value pairs (`slog.Info("msg", "key", val)`)
- **Context present**: actor_id, user_id, channel_id, operation names logged consistently
- **Admin log viewer**: 2000-entry ring buffer in `Server/admin/logstream.go`
- **Minor**: `Server/scripts/seed.go` uses `fmt.Printf` (non-production CLI, acceptable)
### Metrics & Tracing: PRESENT (build-tag gated)
- OpenTelemetry SDK with no-op default; full telemetry via `-tags otel`
- Prometheus metrics export at `/metrics`
- Request ID propagation via `X-Request-Id` header ✅
- Health endpoints: `GET /health`, `GET /api/v1/health`, `GET /api/v1/livekit/health`
### Error Surfacing: GOOD ✅
- Errors logged server-side with `slog.Error` before HTTP response
- Consistent HTTP status codes (400 validation, 503 feature unavailable, 500 internal)
- `writeErr()` / `writeJSON()` helper in admin ensure consistent error response shape
### Client-Side: LIMITED ⚠️
| SEVERITY | Finding |
|----------|---------|
| MEDIUM | No global error boundary component — unhandled promise rejections may fail silently |
| LOW | No crash/error reporting integration (Sentry, etc.) |
---
## 8. Plugin System Governance
### Plugin Architecture
- WASM isolation via Wazero (behind `-tags wazero` build flag)
- Manifest-declared capabilities: `commands`, `events`, `storage`, `http`, `ui`
- Memory capped per runtime (default 64 MiB); CPU budget configurable
### CRITICAL Issues
> **Closure status (2026-07-20):** the table below is the original 2026-04-07
> record and is kept verbatim. Current state lives in the closure table at the
> top of this document — findings 14 are closed; #5 (HTTP exfiltration to an
> allowlisted host) is accepted residual risk, which keeps plugins
> default-disabled at the beta gate. Line numbers below refer to the audited
> tree, not today's.
2026-04-07 10:13:47 +02:00
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| **CRITICAL** | `Server/plugin/sandbox_wazero.go:162,211` | `invokeCommand` has **no timeout** — a looping plugin hangs the goroutine indefinitely |
| **CRITICAL** | `Server/plugin/host_storage.go` | Storage capability has **no key isolation** — plugin can read/write ANY key in the plugin store, not just its own |
| **CRITICAL** | `Server/plugin/host_http.go:162-240` | HTTP capability allows plugins to **exfiltrate data** by POSTing captured payload to any allowlisted host |
| **CRITICAL** | `Server/plugin/registry.go:129-133` | All commands auto-registered if manifest declares `commands` capability — **no per-command ACL** |
| **CRITICAL** | `Server/plugin/host_events.go` | **No rate limit** on event delivery to plugins — malicious plugin could slow server by processing events slowly |
| MEDIUM | `Server/plugin/host_http.go:64-96` | DNS rebinding TOCTOU between `rejectPrivateAddrs` check and TCP dial — not fully atomic |
| LOW | `Server/plugin/registry.go` | Default (non-Wazero) stub build doesn't emit WARN if plugins are configured but stub is running |
| LOW | `Server/plugin/loader.go` | Plugin discovery walks filesystem on every server start — no manifest caching |
### Strengths
- ✅ Per-plugin WASM module isolation (memory, execution)
- ✅ Private IP SSRF protection via `rejectPrivateAddrs()`
- ✅ Symlink rejection in ZIP extraction (zip-slip protection)
- ✅ Decompression bomb protection on plugin upload
- ✅ Graceful lifecycle (disable/uninstall/close) with module cleanup
- ✅ Plugin registry tracks all loaded instances with mutex-protected map
---
## 9. Prioritized Top-10 Action List
| Priority | SEVERITY | Area | Action | File |
|----------|----------|------|--------|------|
| 1 | CRITICAL | Plugin | Add `context.WithTimeout(ctx, cfg.CPUBudgetMs)` around `invokeCommand` to prevent infinite hangs | `Server/plugin/sandbox_wazero.go:162` |
| 2 | CRITICAL | Plugin | Namespace storage keys by plugin ID: `fmt.Sprintf("%d/%s", pluginID, key)` in `PluginGet/Set/Delete` | `Server/plugin/host_storage.go` |
| 3 | CRITICAL | Plugin | Add per-command ACL in registry — check `plugin.Manifest.AllowedCommands` or require explicit command declaration | `Server/plugin/registry.go:129` |
| 4 | CRITICAL | Plugin | Rate-limit event delivery to plugins (token bucket per plugin) to prevent event flooding DoS | `Server/plugin/host_events.go` |
| 5 | HIGH | Tests | Add `store/` package tests — data persistence is completely untested; cover SQLiteStore CRUD and query methods | `Server/store/` |
| 6 | HIGH | Tests | Add TypeScript unit tests for `src/lib/` (api.ts, ws.ts, dispatcher.ts) and `src/stores/` — currently <10% unit coverage | `Client/tauri-client/src/lib/` |
| 7 | HIGH | Deps | Pin critical npm packages to exact versions: `@tauri-apps/plugin-updater`, `vite`, `@tauri-apps/plugin-store` | `Client/tauri-client/package.json` |
| 8 | MEDIUM | Architecture | Refactor `Server/api/auth_handler.go` to route through `AuthService` — currently bypasses service layer | `Server/api/auth_handler.go` |
| 9 | MEDIUM | Code Quality | Silence audit trail loss — wrap `database.LogAudit()` calls with `slog.Error` on failure instead of `_ =` | `Server/admin/handlers_*.go` |
| 10 | MEDIUM | CI/CD | Add E2E gate to `ci.yml` on push to main (not only on PR) + add `.nvmrc` for local Node version enforcement | `.github/workflows/ci.yml` |
### Bonus (quick wins)
- Add `.catch()` handlers to `.then()` chains in `InviteManager.ts`, `file-upload.ts`, `attachments.ts`
- Add `context.WithTimeout` guard in `Server/ws/voice_leave.go` (`livekit.RemoveParticipant` has `//nolint:contextcheck`)
- Add HTTPS-only URL validation in `Server/ws/command.go` (existing TODO)
- Add frontend error boundary component wrapping `MainPage`
- Add Unicode/invisible-character validation to username registration
---
*Report generated from 6 parallel audit agents. All findings are read-only analysis; no code was modified.*