From 2a88b0c711dc63b1d1be61782728f8102ad25cc6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 4 Jul 2026 01:11:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(plugins):=20zero-config=20L1=20hardening?= =?UTF-8?q?=20=E2=80=94=20SSRF=20egress,=20RSS=20reaper,=20capability=20au?= =?UTF-8?q?dit,=20no=20popups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/src/api/client.ts | 1 + client/src/components/Plugins/PluginFrame.tsx | 2 +- server/src/db/migrations.ts | 21 +++++ .../src/nest/plugins/host/create-rpc-host.ts | 2 + server/src/nest/plugins/host/plugin-audit.ts | 63 +++++++++++++++ server/src/nest/plugins/host/rpc-host.ts | 28 ++++++- .../nest/plugins/plugin-frame.controller.ts | 5 +- server/src/nest/plugins/plugins.controller.ts | 5 ++ server/src/nest/plugins/plugins.service.ts | 6 ++ .../src/nest/plugins/runtime/egress-policy.ts | 75 +++++++++++++++++ .../nest/plugins/runtime/plugin-host-entry.ts | 80 +++++++++++++------ .../plugins/supervisor/plugin-supervisor.ts | 27 ++++++- .../tests/unit/plugins/egress-policy.test.ts | 71 ++++++++++++++++ .../tests/unit/plugins/plugin-audit.test.ts | 69 ++++++++++++++++ .../tests/unit/plugins/plugin-frame.test.ts | 3 +- 15 files changed, 425 insertions(+), 33 deletions(-) create mode 100644 server/src/nest/plugins/host/plugin-audit.ts create mode 100644 server/src/nest/plugins/runtime/egress-policy.ts create mode 100644 server/tests/unit/plugins/egress-policy.test.ts create mode 100644 server/tests/unit/plugins/plugin-audit.test.ts diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 534047ff1..8782ba4c9 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -469,6 +469,7 @@ export const adminApi = { pluginUninstall: (id: string, deleteData: boolean) => apiClient.post(`/admin/plugins/${id}/uninstall`, { deleteData }).then(r => r.data), pluginRescan: () => apiClient.post('/admin/plugins/rescan').then(r => r.data), pluginErrors: (id: string) => apiClient.get(`/admin/plugins/${id}/errors`).then(r => r.data), + pluginAudit: (id: string) => apiClient.get(`/admin/plugins/${id}/audit`).then(r => r.data), // Local LLM (Ollama) management for the AI-parsing addon. llmLocalModels: (baseUrl: string): Promise<{ models: { name: string; size: number }[] }> => apiClient.get('/admin/llm/local/models', { params: { baseUrl } }).then(r => r.data), diff --git a/client/src/components/Plugins/PluginFrame.tsx b/client/src/components/Plugins/PluginFrame.tsx index 74edeaf03..4be5a32a4 100644 --- a/client/src/components/Plugins/PluginFrame.tsx +++ b/client/src/components/Plugins/PluginFrame.tsx @@ -104,7 +104,7 @@ export default function PluginFrame({ pluginId, tripId = null, className, title