98659783c0
Block traversal, out-of-root absolute path, and symlink escapes for @file references, file:// URIs, and config skill file loading while logging rejected attempts. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
import type { SkillDefinition } from "../../../config/schema"
|
|
import { configEntryToLoadedSkill } from "./config-skill-entry-loader"
|
|
|
|
describe("configEntryToLoadedSkill", () => {
|
|
const fixtureRoot = join(tmpdir(), `config-skill-entry-loader-${Date.now()}`)
|
|
const configDir = join(fixtureRoot, "config")
|
|
const allowedSkillPath = join(configDir, "allowed-skill.md")
|
|
const linkedSecretSkillPath = join(configDir, "linked-secret-skill.md")
|
|
const outsideSkillPath = join(fixtureRoot, "secret-skill.md")
|
|
|
|
beforeAll(() => {
|
|
mkdirSync(configDir, { recursive: true })
|
|
writeFileSync(
|
|
allowedSkillPath,
|
|
[
|
|
"---",
|
|
"description: Allowed skill",
|
|
"---",
|
|
"Use ./allowed.txt for context.",
|
|
].join("\n"),
|
|
"utf8"
|
|
)
|
|
writeFileSync(
|
|
outsideSkillPath,
|
|
[
|
|
"---",
|
|
"description: Secret skill",
|
|
"---",
|
|
"Do not leak this.",
|
|
].join("\n"),
|
|
"utf8"
|
|
)
|
|
symlinkSync(outsideSkillPath, linkedSecretSkillPath)
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(fixtureRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
test("loads skills from files within configDir", () => {
|
|
//#given
|
|
const entry: SkillDefinition = { from: "./allowed-skill.md" }
|
|
|
|
//#when
|
|
const loaded = configEntryToLoadedSkill("allowed-skill", entry, configDir)
|
|
|
|
//#then
|
|
expect(loaded).not.toBeNull()
|
|
expect(loaded?.definition.template).toContain("Use ./allowed.txt for context.")
|
|
})
|
|
|
|
test("rejects absolute skill files outside configDir", () => {
|
|
//#given
|
|
const entry: SkillDefinition = { from: outsideSkillPath }
|
|
|
|
//#when
|
|
const loaded = configEntryToLoadedSkill("secret-skill", entry, configDir)
|
|
|
|
//#then
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
test("rejects traversal skill files that escape configDir", () => {
|
|
//#given
|
|
const entry: SkillDefinition = { from: "../secret-skill.md" }
|
|
|
|
//#when
|
|
const loaded = configEntryToLoadedSkill("secret-skill", entry, configDir)
|
|
|
|
//#then
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
test("rejects symlink skill files that escape configDir", () => {
|
|
//#given
|
|
const entry: SkillDefinition = { from: "./linked-secret-skill.md" }
|
|
|
|
//#when
|
|
const loaded = configEntryToLoadedSkill("secret-skill", entry, configDir)
|
|
|
|
//#then
|
|
expect(loaded).toBeNull()
|
|
})
|
|
})
|