mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-25 23:47:20 +00:00
fix(skills): constrain plugin skill paths
This commit is contained in:
@@ -100,4 +100,76 @@ describe("resolvePluginSkillDirs", () => {
|
||||
|
||||
expect(dirs).toEqual([path.resolve(helperRoot, "skills")]);
|
||||
});
|
||||
|
||||
it("rejects plugin skill paths that escape the plugin root", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-plugin-");
|
||||
const outsideDir = await tempDirs.make("openclaw-outside-");
|
||||
const outsideSkills = path.join(outsideDir, "skills");
|
||||
await fs.mkdir(path.join(pluginRoot, "skills"), { recursive: true });
|
||||
await fs.mkdir(outsideSkills, { recursive: true });
|
||||
const escapePath = path.relative(pluginRoot, outsideSkills);
|
||||
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "helper",
|
||||
name: "Helper",
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: ["./skills", escapePath],
|
||||
origin: "workspace",
|
||||
rootDir: pluginRoot,
|
||||
source: pluginRoot,
|
||||
manifestPath: path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
},
|
||||
],
|
||||
} satisfies PluginManifestRegistry);
|
||||
|
||||
const dirs = resolvePluginSkillDirs({
|
||||
workspaceDir,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(dirs).toEqual([path.resolve(pluginRoot, "skills")]);
|
||||
});
|
||||
|
||||
it("rejects plugin skill symlinks that resolve outside plugin root", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-plugin-");
|
||||
const outsideDir = await tempDirs.make("openclaw-outside-");
|
||||
const outsideSkills = path.join(outsideDir, "skills");
|
||||
const linkPath = path.join(pluginRoot, "skills-link");
|
||||
await fs.mkdir(outsideSkills, { recursive: true });
|
||||
await fs.symlink(
|
||||
outsideSkills,
|
||||
linkPath,
|
||||
process.platform === "win32" ? ("junction" as const) : ("dir" as const),
|
||||
);
|
||||
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "helper",
|
||||
name: "Helper",
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: ["./skills-link"],
|
||||
origin: "workspace",
|
||||
rootDir: pluginRoot,
|
||||
source: pluginRoot,
|
||||
manifestPath: path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
},
|
||||
],
|
||||
} satisfies PluginManifestRegistry);
|
||||
|
||||
const dirs = resolvePluginSkillDirs({
|
||||
workspaceDir,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(dirs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveMemorySlotDecision,
|
||||
} from "../../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
|
||||
|
||||
const log = createSubsystemLogger("skills");
|
||||
|
||||
@@ -72,6 +73,10 @@ export function resolvePluginSkillDirs(params: {
|
||||
log.warn(`plugin skill path not found (${record.id}): ${candidate}`);
|
||||
continue;
|
||||
}
|
||||
if (!isPathInsideWithRealpath(record.rootDir, candidate, { requireRealpath: true })) {
|
||||
log.warn(`plugin skill path escapes plugin root (${record.id}): ${candidate}`);
|
||||
continue;
|
||||
}
|
||||
if (seen.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user