From af94289c172adebc7ac2ea1be5e523dbec0daee1 Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 14 Oct 2026 16:36:46 -0300 Subject: [PATCH] fix(ck): reject only parent-traversal segments in the forget guard Signed-off-by: Wen --- skills/ck/commands/forget.mjs | 4 ++-- tests/skills/ck-forget.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/skills/ck/commands/forget.mjs b/skills/ck/commands/forget.mjs index 60597de31..c9fa81d7c 100644 --- a/skills/ck/commands/forget.mjs +++ b/skills/ck/commands/forget.mjs @@ -12,7 +12,7 @@ */ import { rmSync } from 'fs'; -import { isAbsolute, relative, resolve } from 'path'; +import { isAbsolute, relative, resolve, sep } from 'path'; import { resolveContext, readProjects, writeProjects, CONTEXTS_DIR } from './shared.mjs'; const arg = process.argv[2]; @@ -30,7 +30,7 @@ const { name, contextDir, projectPath } = resolved; // Remove context directory const contextDirPath = resolve(CONTEXTS_DIR, contextDir); const rel = relative(CONTEXTS_DIR, contextDirPath); -if (!rel || rel.startsWith('..') || isAbsolute(rel)) { +if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { console.log(`ck: refusing to remove '${contextDirPath}' — it is not a project directory inside ${CONTEXTS_DIR}.`); process.exit(1); } diff --git a/tests/skills/ck-forget.test.js b/tests/skills/ck-forget.test.js index 05df98106..88531d758 100644 --- a/tests/skills/ck-forget.test.js +++ b/tests/skills/ck-forget.test.js @@ -76,6 +76,37 @@ test('forget refuses to remove the contexts root for a legacy empty contextDir', assert.ok(fs.existsSync(path.join(contextsDir(home), 'alpha', 'context.json')), 'alpha context was removed'); }); +test('forget removes a legacy contextDir whose name starts with dots', home => { + init(home, 'alpha', path.join(home, 'projA')); + const projectsFile = path.join(home, '.claude', 'ck', 'projects.json'); + const projects = JSON.parse(fs.readFileSync(projectsFile, 'utf8')); + projects[path.join(home, 'projB')] = { name: 'legacy', contextDir: '..archive', lastUpdated: '2026-01-01' }; + fs.writeFileSync(projectsFile, JSON.stringify(projects)); + const targetDir = path.join(contextsDir(home), '..archive'); + fs.mkdirSync(targetDir); + fs.writeFileSync(path.join(targetDir, 'context.json'), JSON.stringify({ version: 2, sessions: [] })); + + const result = run(home, 'forget.mjs', ['legacy']); + assert.strictEqual(result.status, 0, result.stdout + result.stderr); + assert.ok(!fs.existsSync(targetDir), '..archive directory was not removed'); + assert.ok(fs.existsSync(path.join(contextsDir(home), 'alpha', 'context.json')), 'alpha context was removed'); +}); + +test('forget refuses a legacy contextDir that escapes the contexts root', home => { + init(home, 'alpha', path.join(home, 'projA')); + const projectsFile = path.join(home, '.claude', 'ck', 'projects.json'); + const projects = JSON.parse(fs.readFileSync(projectsFile, 'utf8')); + projects[path.join(home, 'projB')] = { name: 'legacy', contextDir: '../outside', lastUpdated: '2026-01-01' }; + fs.writeFileSync(projectsFile, JSON.stringify(projects)); + const outside = path.join(home, '.claude', 'ck', 'outside'); + fs.mkdirSync(outside); + fs.writeFileSync(path.join(outside, 'context.json'), JSON.stringify({ version: 2, sessions: [] })); + + const result = run(home, 'forget.mjs', ['legacy']); + assert.strictEqual(result.status, 1, result.stdout + result.stderr); + assert.ok(fs.existsSync(outside), 'directory outside the contexts root was removed'); +}); + console.log(`\nPassed: ${passed}`); console.log(`Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);