mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
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).
This commit is contained in:
@@ -81,11 +81,12 @@ describe('PluginsProxyController', () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toBe('application/json');
|
||||
expect(res.body).toBe('{"ok":true}');
|
||||
// the child receives only a whitelisted user view, never the token/cookie
|
||||
// the child receives only a whitelisted user view, never the token/cookie;
|
||||
// the acting user (5) is bound server-side as the 4th invoke arg
|
||||
expect(runtime.invoke).toHaveBeenCalledWith('p', 'invoke.route', expect.objectContaining({
|
||||
routeId: 0,
|
||||
req: expect.objectContaining({ user: { id: 5, username: 'ada', isAdmin: false } }),
|
||||
}));
|
||||
}), 5);
|
||||
});
|
||||
|
||||
it('a public (auth:false) route skips the session check', async () => {
|
||||
@@ -94,9 +95,10 @@ describe('PluginsProxyController', () => {
|
||||
await new PluginsProxyController(runtime).proxy('p', fakeReq('POST', '/webhook'), res as never);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(extractTokenMock).not.toHaveBeenCalled();
|
||||
// a public route has no session user → no bound acting user (undefined)
|
||||
expect(runtime.invoke).toHaveBeenCalledWith('p', 'invoke.route', expect.objectContaining({
|
||||
req: expect.objectContaining({ user: null }),
|
||||
}));
|
||||
}), undefined);
|
||||
});
|
||||
|
||||
it('strips unsafe response headers from the child', async () => {
|
||||
|
||||
Reference in New Issue
Block a user