Plugins: require explicit trust for workspace-discovered plugins (#44174)

* Plugins: disable implicit workspace plugin auto-load

* Tests: cover workspace plugin trust gating

* Changelog: note workspace plugin trust hardening

* Plugins: keep workspace trust gate ahead of memory slot defaults

* Tests: cover workspace memory-slot trust bypass
This commit is contained in:
Vincent Koc
2026-03-12 12:12:41 -04:00
committed by GitHub
parent 0a8fa0e001
commit 3e28e10c2f
4 changed files with 110 additions and 1 deletions

View File

@@ -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

View File

@@ -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)",
});
});
});

View File

@@ -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) {

View File

@@ -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();