555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
/**
|
|
* Tests for path traversal prevention in skill-loader.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
safePath,
|
|
normalizeModuleUrlToPath,
|
|
loadSkill,
|
|
loadTriagePrompt,
|
|
loadCompactTriagePrompt,
|
|
} from "./skill-loader.ts";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// safePath — path containment
|
|
// ---------------------------------------------------------------------------
|
|
describe("safePath", () => {
|
|
it("rejects parent directory traversal", () => {
|
|
expect(safePath("../../etc/passwd")).toBeNull();
|
|
});
|
|
|
|
it("rejects deep traversal", () => {
|
|
expect(safePath("../../../etc/shadow")).toBeNull();
|
|
});
|
|
|
|
it("rejects traversal in nested segment", () => {
|
|
expect(safePath("valid", "../../etc")).toBeNull();
|
|
});
|
|
|
|
it("rejects bare '..' as segment", () => {
|
|
expect(safePath("..")).toBeNull();
|
|
});
|
|
|
|
it("accepts valid skill paths", () => {
|
|
expect(safePath("memory-triage", "SKILL.md")).not.toBeNull();
|
|
});
|
|
|
|
it("accepts valid domain overlay paths", () => {
|
|
expect(safePath("memory-triage", "domains", "companion.md")).not.toBeNull();
|
|
});
|
|
|
|
it("returns null for empty segments that resolve to skills root with subpath escape", () => {
|
|
// path.resolve("skills", "", "../../etc") still escapes
|
|
expect(safePath("", "../../etc")).toBeNull();
|
|
});
|
|
|
|
it("rejects traversal disguised with valid prefix", () => {
|
|
expect(safePath("memory-triage/../../etc/passwd")).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSkill — integration tests for traversal prevention
|
|
// ---------------------------------------------------------------------------
|
|
describe("loadSkill path traversal", () => {
|
|
it("returns null for traversal skillName", () => {
|
|
expect(loadSkill("../../etc/passwd")).toBeNull();
|
|
});
|
|
|
|
it("returns null for deep traversal skillName", () => {
|
|
expect(loadSkill("../../../..")).toBeNull();
|
|
});
|
|
|
|
it("loads a valid skill", () => {
|
|
const result = loadSkill("memory-triage");
|
|
expect(result).not.toBeNull();
|
|
expect(result?.prompt).toBeTruthy();
|
|
});
|
|
|
|
it("blocks domain traversal while loading valid skill", () => {
|
|
// Valid skill name, malicious domain — should load skill but skip the overlay
|
|
const result = loadSkill("memory-triage", { domain: "../../etc/passwd" });
|
|
// Should still succeed (skill itself is valid), domain overlay is just skipped
|
|
expect(result).not.toBeNull();
|
|
expect(result?.prompt).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("normalizeModuleUrlToPath", () => {
|
|
it("normalizes raw Windows paths before fileURLToPath conversion", () => {
|
|
const rawWindowsMetaUrl = "C:\\Users\\example\\openclaw\\index.ts";
|
|
const result = normalizeModuleUrlToPath(rawWindowsMetaUrl);
|
|
// Assert the decoded property directly rather than reconstructing via the function body
|
|
expect(typeof result).toBe("string");
|
|
expect(result).not.toContain("%5C");
|
|
});
|
|
|
|
it("leaves already-correct file URLs unchanged", () => {
|
|
const fileMetaUrl = "file:///C:/Users/example/openclaw/index.ts";
|
|
const expected = fileURLToPath(fileMetaUrl);
|
|
|
|
expect(normalizeModuleUrlToPath(fileMetaUrl)).toBe(expected);
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"passes POSIX absolute paths through unchanged",
|
|
() => {
|
|
const posixPath = "/home/user/openclaw/index.ts";
|
|
expect(normalizeModuleUrlToPath(posixPath)).toBe(posixPath);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("loadCompactTriagePrompt", () => {
|
|
it("keeps the core triage instructions without inlining the full skill body", () => {
|
|
const prompt = loadCompactTriagePrompt();
|
|
|
|
expect(prompt).toContain("Use `memory_add` tool for ALL user facts");
|
|
expect(prompt).toContain("Batch facts by CATEGORY");
|
|
expect(prompt).toContain("ALWAYS rewrite the query");
|
|
expect(prompt).not.toContain("## Worked Examples");
|
|
expect(prompt).not.toContain("### memory_search");
|
|
expect(prompt).not.toContain("Conference requires at least 4 breakout rooms");
|
|
expect(prompt.length).toBeLessThan(3500);
|
|
});
|
|
|
|
it("keeps the always-recall guidance aligned with the full triage prompt", () => {
|
|
const config = { recall: { strategy: "always" as const } };
|
|
const expected =
|
|
"Automatic recall runs for both long-term and session memory. Use manual searches only when you need more specific context.";
|
|
|
|
expect(loadCompactTriagePrompt(config)).toContain(expected);
|
|
expect(loadTriagePrompt(config)).toContain(expected);
|
|
});
|
|
|
|
it("keeps the manual-recall guidance in compact mode", () => {
|
|
const prompt = loadCompactTriagePrompt({
|
|
recall: { strategy: "manual" },
|
|
});
|
|
|
|
expect(prompt).toContain("No automatic recall happens in manual mode.");
|
|
});
|
|
|
|
it("includes short config summaries and truncates oversized custom rules", () => {
|
|
const prompt = loadCompactTriagePrompt({
|
|
categories: {
|
|
travel: { importance: 0.7, ttl: "30d" },
|
|
},
|
|
customRules: {
|
|
include: [
|
|
"Always remember workshop venue requirements.",
|
|
"Keep track of recurring conference planning constraints.",
|
|
"Capture every catering preference, transit note, and presenter dependency in detail.",
|
|
],
|
|
exclude: [
|
|
"Never store one-off demo logs, temporary ETA chatter, or transitory checklist updates.",
|
|
"Skip verbose retrospectives unless the user explicitly says the lesson should persist.",
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(prompt).toContain("travel (importance 0.7, expires: 30d)");
|
|
expect(prompt).toContain("include rule(s) and 2 exclude rule(s) are configured");
|
|
expect(prompt).toContain("Prompt kept compact, full rule text omitted");
|
|
expect(prompt).toContain('Preview: include "Always remember workshop venue requirements."');
|
|
});
|
|
|
|
it("omits default credential patterns in compact mode unless custom patterns are configured", () => {
|
|
const defaultPrompt = loadCompactTriagePrompt();
|
|
const customPrompt = loadCompactTriagePrompt({
|
|
triage: {
|
|
credentialPatterns: ["mem0-secret=", "Bearer "],
|
|
},
|
|
});
|
|
|
|
expect(defaultPrompt).not.toContain("Credential patterns to scan");
|
|
expect(customPrompt).toContain("Credential patterns to scan");
|
|
expect(customPrompt).toContain("mem0-secret=");
|
|
});
|
|
});
|