fix(plugins): security hardening from the PR #1415 audit

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.
This commit is contained in:
Maurice
2026-07-04 18:32:26 +02:00
committed by Maurice
parent 16e27926c5
commit 64136484ee
25 changed files with 442 additions and 85 deletions
@@ -67,8 +67,13 @@ export class PluginFrameController {
/** Per-plugin, locked-down CSP for the sandboxed frame document. */
private frameCsp(pluginId: string): string {
const outbound = this.runtime.outboundHostsOf(pluginId);
const connect = ["'self'", ...outbound.map((h) => (h.includes('://') ? h : `https://${h}`))].join(' ');
// Defense in depth: the manifest validator already constrains these hosts, but
// never interpolate anything that isn't a clean host/wildcard into connect-src
// (a stray space or `*` would inject an extra CSP source token).
const outbound = this.runtime
.outboundHostsOf(pluginId)
.filter((h) => /^(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(h));
const connect = ["'self'", ...outbound.map((h) => `https://${h}`)].join(' ');
return [
"default-src 'none'",
// The frame runs at an OPAQUE origin (sandbox without allow-same-origin),