Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

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=");
});
});