diff --git a/CHANGELOG.md b/CHANGELOG.md index aac8fd373..4a7b1ce73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - Security/WebSocket preauth: shorten unauthenticated handshake retention and reject oversized pre-auth frames before application-layer parsing to reduce pre-pairing exposure on unsupported public deployments. (`GHSA-jv4g-m82p-2j93`)(#44089) (`GHSA-xwx2-ppv2-wx98`)(#44089) Thanks @ez-lbz and @vincentkoc. - Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic `p2p` reactions. (`GHSA-m69h-jm2f-2pv8`)(#44088) Thanks @zpbrent and @vincentkoc. - Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a `200` response. (`GHSA-mhxh-9pjm-w7q5`)(#44090) Thanks @TerminalsandCoffee and @vincentkoc. +- Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (`GHSA-99qw-6mr3-36qr`)(#44174) Thanks @lintsinghua and @vincentkoc. ### Changes diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index ebb5d3668..2d287a71e 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -145,4 +145,52 @@ describe("resolveEnableState", () => { ); expect(state).toEqual({ enabled: false, reason: "disabled in config" }); }); + + it("disables workspace plugins by default when they are only auto-discovered from the workspace", () => { + const state = resolveEnableState("workspace-helper", "workspace", normalizePluginsConfig({})); + expect(state).toEqual({ + enabled: false, + reason: "workspace plugin (disabled by default)", + }); + }); + + it("allows workspace plugins when explicitly listed in plugins.allow", () => { + const state = resolveEnableState( + "workspace-helper", + "workspace", + normalizePluginsConfig({ + allow: ["workspace-helper"], + }), + ); + expect(state).toEqual({ enabled: true }); + }); + + it("allows workspace plugins when explicitly enabled in plugin entries", () => { + const state = resolveEnableState( + "workspace-helper", + "workspace", + normalizePluginsConfig({ + entries: { + "workspace-helper": { + enabled: true, + }, + }, + }), + ); + expect(state).toEqual({ enabled: true }); + }); + + it("does not let the default memory slot auto-enable an untrusted workspace plugin", () => { + const state = resolveEnableState( + "memory-core", + "workspace", + normalizePluginsConfig({ + slots: { memory: "memory-core" }, + }), + ); + expect(state).toEqual({ + enabled: false, + reason: "workspace plugin (disabled by default)", + }); + }); }); diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index e671aae7e..fbb4b92a9 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -201,10 +201,14 @@ export function resolveEnableState( if (entry?.enabled === false) { return { enabled: false, reason: "disabled in config" }; } + const explicitlyAllowed = config.allow.includes(id); + if (origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) { + return { enabled: false, reason: "workspace plugin (disabled by default)" }; + } if (config.slots.memory === id) { return { enabled: true }; } - if (config.allow.length > 0 && !config.allow.includes(id)) { + if (config.allow.length > 0 && !explicitlyAllowed) { return { enabled: false, reason: "not in allowlist" }; } if (entry?.enabled === true) { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 00af213be..2241fbd1f 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1449,6 +1449,62 @@ describe("loadOpenClawPlugins", () => { ).toBe(true); }); + it("does not auto-load workspace-discovered plugins unless explicitly trusted", () => { + useNoBundledPlugins(); + const workspaceDir = makeTempDir(); + const workspaceExtDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-helper"); + fs.mkdirSync(workspaceExtDir, { recursive: true }); + writePlugin({ + id: "workspace-helper", + body: `module.exports = { id: "workspace-helper", register() {} };`, + dir: workspaceExtDir, + filename: "index.cjs", + }); + + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir, + config: { + plugins: { + enabled: true, + }, + }, + }); + + const workspacePlugin = registry.plugins.find((entry) => entry.id === "workspace-helper"); + expect(workspacePlugin?.origin).toBe("workspace"); + expect(workspacePlugin?.status).toBe("disabled"); + expect(workspacePlugin?.error).toContain("workspace plugin (disabled by default)"); + }); + + it("loads workspace-discovered plugins when plugins.allow explicitly trusts them", () => { + useNoBundledPlugins(); + const workspaceDir = makeTempDir(); + const workspaceExtDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-helper"); + fs.mkdirSync(workspaceExtDir, { recursive: true }); + writePlugin({ + id: "workspace-helper", + body: `module.exports = { id: "workspace-helper", register() {} };`, + dir: workspaceExtDir, + filename: "index.cjs", + }); + + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir, + config: { + plugins: { + enabled: true, + allow: ["workspace-helper"], + }, + }, + }); + + const workspacePlugin = registry.plugins.find((entry) => entry.id === "workspace-helper"); + expect(workspacePlugin?.origin).toBe("workspace"); + expect(workspacePlugin?.status).toBe("loaded"); + }); + it("warns when loaded non-bundled plugin has no install/load-path provenance", () => { useNoBundledPlugins(); const stateDir = makeTempDir();