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)
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.
| # |
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/ package is being removed in P4 (single data layer); tests move to in-memory SQLite |
| 7 |
HIGH |
Client src/lib/src/stores <10% unit coverage |
CLOSED since audit — large vitest suite exists (113 files); suite health tracked in P2 |
| 8 |
HIGH |
Unpinned critical npm packages |
OPEN — review in P2 |
| 9 |
MEDIUM |
auth_handler bypasses service layer |
OPEN — P4 consolidation candidate |
| 10 |
MEDIUM |
Audit-trail write failures silently ignored |
OPEN — cheap fix, fold into P1 |
| 11 |
MEDIUM |
E2E not in CI / no .nvmrc |
IN PROGRESS — nightly non-blocking e2e job planned in P2 |
Table of Contents
- Architecture
- Code Quality
- Security
- Dependencies & Supply Chain
- Test Coverage & Quality
- CI/CD & DevEx
- Observability
- Plugin System Governance
- 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 ` |
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 1–4 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.
| 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.