Remediates the findings from the adversarial audit (threat model: malicious
plugin author + malicious artifact). Highlights:
Critical
- proxy: force nosniff + Content-Disposition: attachment on every proxied reply
and drop location/content-disposition + non-2xx from the passthrough, so a
plugin can't serve an HTML document at TREK's origin (sandbox-escape → account
takeover) or an open redirect.
High
- db:own runs synchronously in the host: cap the plugin DB (max_page_count) and
row-cap query() via iterate() so a recursive CTE / huge blob can't stall the
event loop, OOM, or exhaust the shared volume.
- supervisor: measure child RSS host-side (/proc/<pid>/statm) instead of trusting
the spoofable heartbeat; add an activation timeout so a stuck onLoad can't hang
activate() or peg a core unreaped.
- safe-extract: enforce entry-count + cumulative-size limits INSIDE readZip
before inflating (decompression-bomb OOM).
- re-consent: activate() never widens granted permissions without explicit
consent (409 CONSENT_REQUIRED); the row toggle + "Update All" route through the
consent dialog, which now queues instead of overwriting.
Medium/low
- egress: gate dgram hostnames through the IP-vetting resolver; block the
low-level socket escape (process.binding) + lock the wrapped prototypes;
canonicalize IPv6 in isBlockedIp (hex-mapped/compressed metadata); reject
degenerate `*.` / whole-TLD / spaced outbound hosts in the manifest + CSP.
- ws:broadcast is membership-gated to the acting user's trips / own connections;
users.getById is scoped to users the acting user can see (no enumeration).
- safe-fetch streams + aborts at the byte cap (chunked codeload OOM); isPrivateIp
reuses the canonicalizing check.
- native-scan throws instead of silently passing past its entry cap.
- SDK: dev serves binary assets as raw buffers + handles EADDRINUSE; manifest
validator gains the reserved-id + outbound-host checks; wikis corrected.
Tests updated for the new membership-gated behaviour + regression tests added
(IPv6 canonicalization, wildcard hardening, outbound-host validation, ws/user
scoping). 234 plugin tests + 24 SDK tests green.
Every plugin now runs in its own forked child process with a scrubbed env
(no JWT_SECRET, no db path, nothing inherited). It talks to TREK only over a
JSON-RPC channel, and the host's capability router registers ONLY the methods a
plugin's granted permissions unlock — so an ungranted call is unreachable, not
merely refused. The plugin's own data lives in a separate sqlite file it can
never open directly; core reads (trips/users) go through membership-checked,
column-projected host methods; ws broadcasts are force-namespaced.
- protocol/envelope: the wire types + method→permission map (pure, shared by
host and the isolated child)
- host/rpc-host: the capability router = the enforcement point (dispatch,
BAD_PARAMS / PERMISSION_DENIED / RESOURCE_FORBIDDEN / UNKNOWN_METHOD)
- host/plugin-data: the per-plugin sqlite file (db:own), guarded against
ATTACH/PRAGMA escape, idempotent migrations
- host/create-rpc-host: wires the router to the real db/websocket (host-only)
- runtime/plugin-sdk + plugin-host-entry: the child bootstrap + definePlugin
ctx; turns each ctx call into an RPC, never imports a privileged module
- supervisor: spawn on activate, heartbeat/reap, crash backoff + auto-disable,
graceful shutdown — a plugin crash/OOM/hang can never reach the Nest loop
- paths: code/data layout, dist-vs-tsx child entry resolution
Nothing is wired into activation yet (that's the next slice); exercised by unit
tests for the router/sdk/data and an integration test that forks a real child.