A second adversarial pass over the first remediation found four findings only
partially closed and five issues the fixes themselves introduced. This closes
them:
Partial → closed
- re-consent gate keyed on `granted.length > 0`, so a plugin first activated with
ZERO permissions (granted '[]') was treated as never-consented and a later
widening was granted silently. Now discovery marks a never-consented plugin with
granted_permissions '' and activate() gates on "ever consented" (any non-empty
string, including '[]').
- db:own DoS: block WITH RECURSIVE outright (the one construct that spins the
synchronous host unboundedly regardless of the size/row caps, via query OR exec).
- dgram: also wrap `new dgram.Socket(...)` (bypassed createSocket) to inject the
IP-vetting lookup, and lock createSocket/Socket.
- frame self-navigation: documented as a bounded best-effort mitigation (inherent
to sandboxed iframes; exposure is the plugin's own routes + already-held context,
never the httpOnly cookie).
Regressions introduced by the first pass → fixed
- proxy: only real redirects (301/302/303/307/308) are gated, to a RELATIVE in-app
Location (supports OAuth-callback bounce, blocks open redirect); 300/304 pass
through; attachment only on non-redirects.
- supervisor: measure RSS via /proc/<pid>/status VmRSS (page-size independent);
activation-timeout awaits kill() before disposing the db handle.
- manifest HOST_RE: allow single-label hosts (self-hoster sibling services) while
keeping wildcards multi-label; mirrored in the SDK + frame CSP filter.
Regression tests added (re-consent incl. the '[]' case, WITH RECURSIVE + row cap,
single-label host). 236 plugin tests + 24 SDK tests green.
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.
Security that ships from the install itself, no self-hoster setup:
- Egress SSRF/rebinding backstop: the net.Socket connect guard now RESOLVES the
destination and refuses private/loopback/link-local/metadata/CGNAT/ULA
addresses, pinning the resolved IP (a declared host that re-resolves to an
internal address is blocked). Pure policy in egress-policy.ts + tests.
TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on opts back into internal targets.
- RSS memory reaper: the supervisor now kills a child that blows a real RSS
ceiling (TREK_PLUGIN_MAX_RSS_MB, default 300) — --max-old-space-size only
bounds the V8 heap, so Buffers could OOM the box under it.
- Hash-chained capability audit log (migration 159): every core-data / broadcast
call is recorded at the RPC boundary with the host-bound acting user and a
per-plugin hash chain, so wide grants stay attributable + tamper-evident.
Admin endpoint GET /api/admin/plugins/:id/audit.
- Drop allow-popups from the plugin frame (sandbox + CSP): window.open ignores
connect-src, so it was an egress/phishing bypass.
Server suite 207 green, client + migration reconciliation green.
- Plugin frame CSP: the frame runs at an opaque origin (sandbox without
allow-same-origin), so script-src 'self' matches nothing and the widget's own
script never runs (stuck on 'Loading…'). Allow 'unsafe-inline' — the sandbox,
not this directive, is the isolation boundary, and the plugin author controls
the frame code either way.
- Fix a no-constant-binary-expression eslint error in registry.test.ts that was
failing the server lint:check (eslint .) in CI.
Plugins can now render UI. Page plugins appear as a nav entry and open a
full-page sandboxed iframe; the frame talks to TREK only over the postMessage
bridge.
- Server serves plugin client assets at /plugin-frame/:id/* with a strict path
guard and a locked-down, per-plugin CSP (default-src none; connect-src limited
to declared outbound hosts; sandbox WITHOUT allow-same-origin -> opaque origin,
so the frame can't read the session cookie or the parent DOM). Global CSP
frameSrc relaxed from 'none' to 'self' for exactly these frames.
- GET /api/plugins feed lists active plugins for the client.
- Client: pluginStore + PluginFrame (the trekBridge host) authenticates every
inbound message by SENDER WINDOW IDENTITY (event.source), not by a claimed id
or origin; pushes context (theme/locale/tripId/userId), validates navigation,
renders notifications as text, resizes widgets, and proxies trek:invoke to the
plugin's own routes host-side (session cookie stays with the host).
- Page route /plugins/:id + Navbar nav injection for page plugins.
Dashboard widget slot and the trek:request core-data bridge are deferred to a
later slice.