mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
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:
@@ -174,6 +174,20 @@ function installEgressGuard(egress: string[]): void {
|
||||
const allowed = makeHostAllow(egress);
|
||||
const blockPrivate = (process.env.TREK_PLUGIN_ALLOW_PRIVATE_EGRESS ?? '').toLowerCase() !== 'on';
|
||||
|
||||
// The wrappers below are the ONLY egress choke point (the OS --permission model
|
||||
// does not restrict the network). Deny the low-level escape hatch a plugin could
|
||||
// use to build a raw socket handle BELOW those wrappers. node:net/dns/dgram/http
|
||||
// are already imported above, so they keep their captured bindings; only fresh
|
||||
// access via process.binding is cut off.
|
||||
for (const name of ['binding', '_linkedBinding'] as const) {
|
||||
try {
|
||||
Object.defineProperty(process, name, {
|
||||
value: () => { throw new Error(`egress: process.${name} is disabled for plugins`); },
|
||||
writable: false, configurable: false,
|
||||
});
|
||||
} catch { /* already locked / non-configurable */ }
|
||||
}
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
if (typeof realFetch === 'function') {
|
||||
globalThis.fetch = ((input: unknown, init?: unknown) => {
|
||||
@@ -261,6 +275,25 @@ function installEgressGuard(egress: string[]): void {
|
||||
guardDatagram(dgramConnectTarget(args));
|
||||
return realDgramConnect.apply(this, args);
|
||||
};
|
||||
// A dgram HOSTNAME target is resolved via the socket's `lookup` before the packet
|
||||
// is sent; force the IP-vetting guardedLookup so a declared name that resolves to
|
||||
// a private/metadata address is refused (the TCP path's rebind backstop, for UDP).
|
||||
const dgramApi = dgram as unknown as { createSocket: (...a: unknown[]) => unknown };
|
||||
const realCreateSocket = dgramApi.createSocket;
|
||||
dgramApi.createSocket = function (this: unknown, ...args: unknown[]): unknown {
|
||||
const first = args[0];
|
||||
if (first && typeof first === 'object') (first as { lookup?: unknown }).lookup = guardedLookup;
|
||||
else if (typeof first === 'string') args[0] = { type: first, lookup: guardedLookup };
|
||||
return realCreateSocket.apply(this, args);
|
||||
};
|
||||
|
||||
// Lock the wrapped choke points so a plugin can't restore the originals.
|
||||
const lock = (obj: object, key: string, value: unknown) => {
|
||||
try { Object.defineProperty(obj, key, { value, writable: false, configurable: false }); } catch { /* noop */ }
|
||||
};
|
||||
lock(proto, 'connect', proto.connect);
|
||||
lock(dgramProto, 'send', dgramProto.send);
|
||||
lock(dgramProto, 'connect', dgramProto.connect);
|
||||
|
||||
// Gate the dns resolver family: a forward lookup for an undeclared name is a
|
||||
// DNS-tunnel exfiltration channel even when no socket is ever opened. The name
|
||||
|
||||
Reference in New Issue
Block a user