mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(security): restore trusted plugin runtime exec default
This commit is contained in:
@@ -33,7 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Gateway: rate-limit control-plane write RPCs (`config.apply`, `config.patch`, `update.run`) to 3 requests per minute per `deviceId+clientIp`, add restart single-flight coalescing plus a 30-second restart cooldown, and log actor/device/ip with changed-path audit details for config/update-triggered restarts.
|
||||
- Commands/Doctor: skip embedding-provider warnings when `memory.backend` is `qmd`, because QMD manages embeddings internally and does not require `memorySearch` providers. (#17263) Thanks @miloudbelarebia.
|
||||
- Security/Webhooks: harden Feishu and Zalo webhook ingress with webhook-mode token preconditions, loopback-default Feishu bind host, JSON content-type enforcement, per-path rate limiting, replay dedupe for Zalo events, constant-time Zalo secret comparison, and anomaly status counters.
|
||||
- Security/Plugins: add explicit `plugins.runtime.allowLegacyExec` opt-in to re-enable deprecated `runtime.system.runCommandWithTimeout` for legacy modules while keeping runtime command execution disabled by default. (#20874) Thanks @mbelinky.
|
||||
- Security/Plugins: for the next npm release, clarify plugin trust boundary and keep `runtime.system.runCommandWithTimeout` available by default for trusted in-process plugins. Thanks @markmusson for reporting.
|
||||
- Gateway/WebChat: block `sessions.patch` and `sessions.delete` for WebChat clients so session-store mutations stay restricted to non-WebChat operator flows. Thanks @allsmog for reporting.
|
||||
- Security/Skills: for the next npm release, reject symlinks during skill packaging to prevent external file inclusion in distributed `.skill` archives. Thanks @aether-ai-agent for reporting.
|
||||
- Security/Gateway: fail startup when `hooks.token` matches `gateway.auth.token` so hooks and gateway token reuse is rejected at boot. (#20813) Thanks @coygeek.
|
||||
|
||||
@@ -49,6 +49,14 @@ When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (o
|
||||
- Using OpenClaw in ways that the docs recommend not to
|
||||
- Prompt injection attacks
|
||||
|
||||
## Plugin Trust Boundary
|
||||
|
||||
Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
|
||||
|
||||
- Plugins can execute with the same OS privileges as the OpenClaw process.
|
||||
- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary.
|
||||
- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids.
|
||||
|
||||
## Operational Guidance
|
||||
|
||||
For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see:
|
||||
|
||||
@@ -267,8 +267,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
|
||||
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
|
||||
"plugins.load.paths": "Additional plugin files or directories to load.",
|
||||
"plugins.runtime.allowLegacyExec":
|
||||
"Opt-in compatibility switch to re-enable deprecated runtime.system.runCommandWithTimeout for legacy plugins (default: false).",
|
||||
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
|
||||
"plugins.slots.memory":
|
||||
'Select the active memory plugin by id, or "none" to disable memory plugins.',
|
||||
|
||||
@@ -310,7 +310,6 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"plugins.allow": "Plugin Allowlist",
|
||||
"plugins.deny": "Plugin Denylist",
|
||||
"plugins.load.paths": "Plugin Load Paths",
|
||||
"plugins.runtime.allowLegacyExec": "Allow Legacy Plugin Runtime Exec",
|
||||
"plugins.slots": "Plugin Slots",
|
||||
"plugins.slots.memory": "Memory Plugin",
|
||||
"plugins.entries": "Plugin Entries",
|
||||
|
||||
@@ -13,14 +13,6 @@ export type PluginsLoadConfig = {
|
||||
paths?: string[];
|
||||
};
|
||||
|
||||
export type PluginsRuntimeConfig = {
|
||||
/**
|
||||
* Re-enable deprecated runtime.system.runCommandWithTimeout for legacy plugins.
|
||||
* Disabled by default for security hardening.
|
||||
*/
|
||||
allowLegacyExec?: boolean;
|
||||
};
|
||||
|
||||
export type PluginInstallRecord = {
|
||||
source: "npm" | "archive" | "path";
|
||||
spec?: string;
|
||||
@@ -44,7 +36,6 @@ export type PluginsConfig = {
|
||||
/** Optional plugin denylist (plugin ids). */
|
||||
deny?: string[];
|
||||
load?: PluginsLoadConfig;
|
||||
runtime?: PluginsRuntimeConfig;
|
||||
slots?: PluginSlotsConfig;
|
||||
entries?: Record<string, PluginEntryConfig>;
|
||||
installs?: Record<string, PluginInstallRecord>;
|
||||
|
||||
@@ -631,12 +631,6 @@ export const OpenClawSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
runtime: z
|
||||
.object({
|
||||
allowLegacyExec: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
slots: z
|
||||
.object({
|
||||
memory: z.string().optional(),
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user