Files
openclaw/src/plugins/plugin-policy-id.test.ts
Yuval Dinodia 73cf9a8bed fix(plugins): plugins.deny is bypassed by a mixed-case manifest id (#109237)
* fix(plugins): canonicalize manifest plugin ids to lowercase

Plugin config policy lists (plugins.deny, plugins.allow, plugins.entries)
are lowercase-normalized through normalizePluginId, but a plugin's
self-declared manifest id was only trimmed, never lowercased. A plugin
publishing "id": "Malicious-Scraper" therefore never matched an operator
denylist entry of "malicious-scraper" and fell through to default
activation, loading its hooks, channels, secret integrations, and tools.
The same gap let a mixed-case spelling evade the core reserved-id check.

Canonicalize the id at the manifest parse boundary where
PluginManifestRecord.id is minted, so every downstream policy consumer
compares against the same canonical form.

* test(plugins): prove mixed-case deny at gateway startup

* test(plugins): assert denied gateway record is absent

* test(plugins): materialize allowed secret fixture

* fix(plugins): compare a derived policy key instead of rewriting manifest identity

Manifest ids stay exactly as declared. Deny, allow, and per-entry checks now
compare a lowercase policy key derived at each enforcement boundary, matching
the lowercase-normalized config lists.

The previous approach lowercased PluginManifestRecord.id at the parse boundary.
That id is also matched against the plugin runtime export id, and a mismatch is
a hard load failure, so an existing plugin declaring the same mixed-case id in
both its manifest and its runtime export would stop loading after upgrade.

The Gateway fixture now declares a mixed-case manifest id with a matching
mixed-case runtime export, covering that upgrade case, and drops the allowlist
that previously scoped discovery by a lowercase id and masked the denylist
behavior under test.

* fix(plugins): close mixed-case policy gaps

* test(plugins): use tracked policy fixture temp dir

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 23:13:13 -07:00

62 lines
2.2 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { resolvePluginActivationDecisionShared } from "./config-activation-shared.js";
import { normalizePluginsConfig } from "./config-state.js";
import { resolveManifestOwnerBasePolicyBlock } from "./manifest-owner-policy.js";
import { loadPluginManifest } from "./manifest.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("mixed-case plugin policy ids", () => {
it("applies normalized deny, allow, and entry policy to declared ids", () => {
expect(
resolveManifestOwnerBasePolicyBlock({
plugin: { id: "Malicious-Scraper" },
normalizedConfig: normalizePluginsConfig({ deny: ["malicious-scraper"] }),
}),
).toBe("blocked-by-denylist");
expect(
resolveManifestOwnerBasePolicyBlock({
plugin: { id: "Trusted-Plugin" },
normalizedConfig: normalizePluginsConfig({ allow: ["trusted-plugin"] }),
}),
).toBeNull();
expect(
resolveManifestOwnerBasePolicyBlock({
plugin: { id: "Sneaky-Plugin" },
normalizedConfig: normalizePluginsConfig({
entries: { "sneaky-plugin": { enabled: false } },
}),
}),
).toBe("plugin-disabled");
});
it("blocks a mixed-case id in shared activation policy", () => {
expect(
resolvePluginActivationDecisionShared({
id: "Malicious-Scraper",
origin: "bundled",
config: normalizePluginsConfig({ deny: ["malicious-scraper"] }),
enabledByDefault: true,
isBundledChannelEnabledByChannelConfig: () => false,
}),
).toMatchObject({ enabled: false, activated: false, cause: "blocked-by-denylist" });
});
it("rejects a mixed-case spelling of a core reserved id", () => {
const dir = tempDirs.make("plugin-id-case-");
fs.writeFileSync(
path.join(dir, "openclaw.plugin.json"),
JSON.stringify({ id: "Node-MCP", configSchema: { type: "object" } }),
"utf-8",
);
const result = loadPluginManifest(dir);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("reserved by OpenClaw core");
}
});
});