mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 23:14:31 +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")]);
|
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,
|
resolveMemorySlotDecision,
|
||||||
} from "../../plugins/config-state.js";
|
} from "../../plugins/config-state.js";
|
||||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||||
|
import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("skills");
|
const log = createSubsystemLogger("skills");
|
||||||
|
|
||||||
@@ -72,6 +73,10 @@ export function resolvePluginSkillDirs(params: {
|
|||||||
log.warn(`plugin skill path not found (${record.id}): ${candidate}`);
|
log.warn(`plugin skill path not found (${record.id}): ${candidate}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!isPathInsideWithRealpath(record.rootDir, candidate, { requireRealpath: true })) {
|
||||||
|
log.warn(`plugin skill path escapes plugin root (${record.id}): ${candidate}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (seen.has(candidate)) {
|
if (seen.has(candidate)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user