fix(security): enforce plugin and hook path containment

This commit is contained in:
Peter Steinberger
2026-02-19 15:34:58 +01:00
parent 10379e7dcd
commit 81b19aaa1a
14 changed files with 387 additions and 8 deletions

View File

@@ -266,5 +266,73 @@ describe("loader", () => {
// This test mainly verifies that loading and triggering doesn't crash
expect(getRegisteredEventKeys()).toContain("command:new");
});
it("rejects directory hook handlers that escape hook dir via symlink", async () => {
const outsideHandlerPath = path.join(fixtureRoot, `outside-handler-${caseId}.js`);
await fs.writeFile(outsideHandlerPath, "export default async function() {}", "utf-8");
const hookDir = path.join(tmpDir, "hooks", "symlink-hook");
await fs.mkdir(hookDir, { recursive: true });
await fs.writeFile(
path.join(hookDir, "HOOK.md"),
[
"---",
"name: symlink-hook",
"description: symlink test",
'metadata: {"openclaw":{"events":["command:new"]}}',
"---",
"",
"# Symlink Hook",
].join("\n"),
"utf-8",
);
try {
await fs.symlink(outsideHandlerPath, path.join(hookDir, "handler.js"));
} catch {
return;
}
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
expect(getRegisteredEventKeys()).not.toContain("command:new");
});
it("rejects legacy handler modules that escape workspace via symlink", async () => {
const outsideHandlerPath = path.join(fixtureRoot, `outside-legacy-${caseId}.js`);
await fs.writeFile(outsideHandlerPath, "export default async function() {}", "utf-8");
const linkedHandlerPath = path.join(tmpDir, "legacy-handler.js");
try {
await fs.symlink(outsideHandlerPath, linkedHandlerPath);
} catch {
return;
}
const cfg: OpenClawConfig = {
hooks: {
internal: {
enabled: true,
handlers: [
{
event: "command:new",
module: "legacy-handler.js",
},
],
},
},
};
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(0);
expect(getRegisteredEventKeys()).not.toContain("command:new");
});
});
});