fix(security): restore trusted plugin runtime exec default

This commit is contained in:
Peter Steinberger
2026-02-19 16:01:22 +01:00
parent 8288702f51
commit 2e421f32df
9 changed files with 17 additions and 84 deletions

View File

@@ -1,48 +1,19 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.hoisted(() => vi.fn());
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
vi.mock("../../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/config.js")>();
return {
...actual,
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
};
});
vi.mock("../../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
import { createPluginRuntime } from "./index.js";
describe("plugin runtime security hardening", () => {
const blockedError =
"runtime.system.runCommandWithTimeout is disabled for security hardening. Use fixed-purpose runtime APIs instead.";
describe("plugin runtime command execution", () => {
beforeEach(() => {
loadConfigMock.mockReset();
runCommandWithTimeoutMock.mockReset();
loadConfigMock.mockReturnValue({});
});
it("blocks runtime.system.runCommandWithTimeout by default", async () => {
const runtime = createPluginRuntime();
await expect(
runtime.system.runCommandWithTimeout(["echo", "hello"], { timeoutMs: 1000 }),
).rejects.toThrow(blockedError);
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
});
it("allows runtime.system.runCommandWithTimeout when explicitly opted in", async () => {
loadConfigMock.mockReturnValue({
plugins: {
runtime: {
allowLegacyExec: true,
},
},
});
it("exposes runtime.system.runCommandWithTimeout by default", async () => {
const commandResult = {
stdout: "hello\n",
stderr: "",
@@ -60,15 +31,12 @@ describe("plugin runtime security hardening", () => {
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["echo", "hello"], { timeoutMs: 1000 });
});
it("fails closed when config loading throws", async () => {
loadConfigMock.mockImplementation(() => {
throw new Error("config read failed");
});
it("forwards runtime.system.runCommandWithTimeout errors", async () => {
runCommandWithTimeoutMock.mockRejectedValue(new Error("boom"));
const runtime = createPluginRuntime();
await expect(
runtime.system.runCommandWithTimeout(["echo", "hello"], { timeoutMs: 1000 }),
).rejects.toThrow(blockedError);
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
).rejects.toThrow("boom");
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["echo", "hello"], { timeoutMs: 1000 });
});
});

View File

@@ -1,4 +1,5 @@
import { createRequire } from "node:module";
import type { PluginRuntime } from "./types.js";
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
@@ -138,7 +139,6 @@ import {
} from "../../web/auth-store.js";
import { loadWebMedia } from "../../web/media.js";
import { formatNativeDependencyHint } from "./native-deps.js";
import type { PluginRuntime } from "./types.js";
let cachedVersion: string | null = null;
@@ -236,27 +236,6 @@ function loadWhatsAppActions() {
return whatsappActionsPromise;
}
const RUNTIME_LEGACY_EXEC_DISABLED_ERROR =
"runtime.system.runCommandWithTimeout is disabled for security hardening. Use fixed-purpose runtime APIs instead.";
function isLegacyPluginRuntimeExecEnabled(): boolean {
try {
return loadConfig().plugins?.runtime?.allowLegacyExec === true;
} catch {
// Fail closed if config is unreadable/invalid.
return false;
}
}
const runtimeCommandExecutionGuarded: PluginRuntime["system"]["runCommandWithTimeout"] = async (
...args
) => {
if (!isLegacyPluginRuntimeExecEnabled()) {
throw new Error(RUNTIME_LEGACY_EXEC_DISABLED_ERROR);
}
return await runCommandWithTimeout(...args);
};
export function createPluginRuntime(): PluginRuntime {
return {
version: resolveVersion(),
@@ -266,7 +245,7 @@ export function createPluginRuntime(): PluginRuntime {
},
system: {
enqueueSystemEvent,
runCommandWithTimeout: runtimeCommandExecutionGuarded,
runCommandWithTimeout,
formatNativeDependencyHint,
},
media: {

View File

@@ -184,10 +184,6 @@ export type PluginRuntime = {
};
system: {
enqueueSystemEvent: EnqueueSystemEvent;
/**
* @deprecated Disabled by default for security hardening.
* Set `plugins.runtime.allowLegacyExec: true` to opt in for legacy compatibility.
*/
runCommandWithTimeout: RunCommandWithTimeout;
formatNativeDependencyHint: FormatNativeDependencyHint;
};