mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
fix(plugins): make the permission-model child load from the real plugin path
The prod data dir is a symlink (server/data -> volume), so resolving the plugin
under it tripped the permission model, and Node's module-type lookup walked up
into the (denied) data dir. Fork the child from the plugin's realpath and drop a
{"type":"commonjs"} package.json at its root so resolution stops there — trek.db
and the secret files stay unreadable, verified against Koffi.
This commit is contained in:
@@ -27,6 +27,36 @@ export function pluginCodeDir(id: string): string {
|
||||
return path.join(pluginsCodeRoot(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin code dir with symlinks resolved. The data root is often a symlink
|
||||
* (Docker mounts `server/data` -> a volume), and the OS permission model checks
|
||||
* REAL paths — forking the child from the real path keeps path resolution from
|
||||
* ever touching the symlinked parent (which would need a broad read grant).
|
||||
* Falls back to the lexical path if the dir does not exist yet.
|
||||
*/
|
||||
export function pluginRealCodeDir(id: string): string {
|
||||
try {
|
||||
return fs.realpathSync(pluginCodeDir(id));
|
||||
} catch {
|
||||
return pluginCodeDir(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a plugin its own `package.json` (only if it ships none) so Node resolves
|
||||
* the child's module type AT the plugin root and never walks up into the data
|
||||
* dir — which, under the permission model, is denied (that's the whole point).
|
||||
* Defaults to CommonJS, matching the plain `module.exports` plugins ship.
|
||||
*/
|
||||
export function ensurePluginModuleType(codeDir: string): void {
|
||||
try {
|
||||
const pkg = path.join(codeDir, 'package.json');
|
||||
if (!fs.existsSync(pkg)) fs.writeFileSync(pkg, '{"type":"commonjs"}\n');
|
||||
} catch {
|
||||
/* best effort — a missing package.json only matters under --permission */
|
||||
}
|
||||
}
|
||||
|
||||
/** A plugin's writable data directory (its own sqlite file + any blobs). */
|
||||
export function pluginDataDir(id: string): string {
|
||||
return path.join(pluginsDataRoot(), id);
|
||||
@@ -90,6 +120,7 @@ export function pluginPermissionArgs(pluginId: string): string[] {
|
||||
// resolve the child's module type. Grant just that file — never its dir,
|
||||
// which also holds the `data` symlink to trek.db and the secret files.
|
||||
`--allow-fs-read=${path.join(codeRoot, '..', 'package.json')}`,
|
||||
`--allow-fs-read=${pluginCodeDir(pluginId)}`,
|
||||
// The plugin's own code, by REAL path (see pluginRealCodeDir).
|
||||
`--allow-fs-read=${pluginRealCodeDir(pluginId)}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { fork, type ChildProcess } from 'node:child_process';
|
||||
import { resolveChildEntry, pluginCodeDir, pluginPermissionArgs } from '../paths';
|
||||
import { resolveChildEntry, pluginCodeDir, pluginRealCodeDir, pluginPermissionArgs, ensurePluginModuleType } from '../paths';
|
||||
import type { Envelope, RpcError, RpcRequest } from '../protocol/envelope';
|
||||
import type { PluginRpcHost } from '../host/rpc-host';
|
||||
|
||||
@@ -177,9 +177,13 @@ export class PluginSupervisor {
|
||||
const { entry, execArgv, forkCwd, jsMode } = resolveChildEntry();
|
||||
// Prod (compiled) children get the OS permission model — a real kernel-level
|
||||
// fs/child_process/native jail on top of the env scrub and RPC boundary.
|
||||
// Use the plugin's REAL path + ensure it has a package.json so the sandboxed
|
||||
// child can resolve its own module type without a broad read grant.
|
||||
const codeDir = jsMode ? pluginRealCodeDir(sup.id) : pluginCodeDir(sup.id);
|
||||
if (jsMode) ensurePluginModuleType(codeDir);
|
||||
const argv = jsMode ? [...execArgv, ...pluginPermissionArgs(sup.id)] : execArgv;
|
||||
const child = fork(entry, [sup.id, pluginCodeDir(sup.id)], {
|
||||
cwd: forkCwd ?? pluginCodeDir(sup.id),
|
||||
const child = fork(entry, [sup.id, codeDir], {
|
||||
cwd: forkCwd ?? codeDir,
|
||||
execArgv: argv,
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||
// Whitelist env — nothing inherited. No JWT_SECRET, no DB creds, no PATH-leaked secrets.
|
||||
|
||||
Reference in New Issue
Block a user