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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user