Files
TREK/server/tests/unit/plugins/protocol-paths.test.ts
T
Maurice 332e5c3c42 feat(plugins): sandbox hardening — OS permission model, egress choke point, bound acting user, author signatures
Closes the four gaps the security review surfaced:

- OS permission model on the prod plugin child (Node --permission with
  fs-read scoped to the compiled server dir + the plugin's own code dir, no
  fs-write/child_process/worker/native). A plugin can no longer read trek.db
  or the .jwt_secret/.encryption_key files, nor shell out — the direct-fs and
  RCE escapes that bypassed the RPC layer. Opt-out via TREK_PLUGIN_PERMISSIONS=off.
- Egress guard extended from fetch to the net.Socket connect choke point, so
  node:http/https/net/tls obey the declared-egress allowlist too (no declared
  egress = no outbound). Under the permission model there is no clean escape to
  an unwrapped runtime. Kernel/network-namespace containment remains the
  container step.
- Trip reads are membership-checked against the acting user the HOST binds from
  the authenticated invocation, not an asUserId the plugin supplies; a job/onLoad
  (no user) can't read user-scoped trips.
- Optional minisign (Ed25519) author signatures verified offline, TOFU-pinned
  (migration 158). Unsigned plugins install on sha256 alone; a signed plugin
  can't silently drop its signature or swap its author key.

Server suite green (permission-model activation verified against the Koffi
reference plugin on dev1).
2026-07-04 19:11:23 +02:00

72 lines
3.1 KiB
TypeScript

/**
* Small pure helpers of the plugin module (#plugins): permission recognition and
* the code/data path resolution (both the env-override and default branches).
*/
import { describe, it, expect, afterEach } from 'vitest';
import { isKnownPermission, METHOD_PERMISSION, KNOWN_METHODS } from '../../../src/nest/plugins/protocol/envelope';
import path from 'node:path';
import { pluginsCodeRoot, pluginsDataRoot, pluginCodeDir, pluginDbFile, resolveChildEntry, serverCodeRoot, pluginPermissionArgs } from '../../../src/nest/plugins/paths';
afterEach(() => {
delete process.env.TREK_PLUGINS_DIR;
delete process.env.TREK_PLUGINS_DATA_DIR;
});
describe('envelope helpers', () => {
it('recognises known permissions, http:outbound:<host>, and rejects unknown', () => {
expect(isKnownPermission('db:own')).toBe(true);
expect(isKnownPermission('http:outbound')).toBe(true);
expect(isKnownPermission('http:outbound:api.example.com')).toBe(true);
expect(isKnownPermission('fs:read')).toBe(false);
expect(isKnownPermission('')).toBe(false);
});
it('every known method maps to a permission', () => {
for (const m of KNOWN_METHODS) {
expect(METHOD_PERMISSION[m]).toBeTruthy();
}
});
});
describe('paths', () => {
it('uses the env override when set', () => {
process.env.TREK_PLUGINS_DIR = '/custom/code';
process.env.TREK_PLUGINS_DATA_DIR = '/custom/data';
expect(pluginsCodeRoot()).toBe('/custom/code');
expect(pluginsDataRoot()).toBe('/custom/data');
expect(pluginCodeDir('x')).toBe(path.join('/custom/code', 'x'));
expect(path.basename(pluginDbFile('x'))).toBe('plugin.db');
});
it('falls back to the data-dir default when unset', () => {
expect(pluginsCodeRoot()).toContain('plugins');
expect(pluginsDataRoot()).toContain('plugins-data');
});
it('resolves a child entry with fork args', () => {
const r = resolveChildEntry();
expect(r.entry).toMatch(/plugin-host-entry\.(js|ts)$/);
expect(Array.isArray(r.execArgv)).toBe(true);
expect(typeof r.jsMode).toBe('boolean');
});
it('builds scoped OS-permission flags for a plugin child (default on)', () => {
delete process.env.TREK_PLUGIN_PERMISSIONS;
const args = pluginPermissionArgs('flight-tracker');
expect(args).toContain('--permission');
// read is scoped to the compiled server dir + this plugin's own code dir…
expect(args.some((a) => a === `--allow-fs-read=${serverCodeRoot()}`)).toBe(true);
expect(args.some((a) => a === `--allow-fs-read=${pluginCodeDir('flight-tracker')}`)).toBe(true);
// …and never grants fs-write / child_process / the data root.
expect(args.some((a) => a.startsWith('--allow-fs-write'))).toBe(false);
expect(args.some((a) => a.startsWith('--allow-child-process'))).toBe(false);
expect(serverCodeRoot()).not.toContain(`${require('node:path').sep}data`);
});
it('lets an operator opt out of the permission model', () => {
process.env.TREK_PLUGIN_PERMISSIONS = 'off';
expect(pluginPermissionArgs('flight-tracker')).toEqual([]);
delete process.env.TREK_PLUGIN_PERMISSIONS;
});
});