diff --git a/ccw/src/core/routes/system-routes.ts b/ccw/src/core/routes/system-routes.ts index 15dee91a..c1ebc94e 100644 --- a/ccw/src/core/routes/system-routes.ts +++ b/ccw/src/core/routes/system-routes.ts @@ -286,19 +286,35 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise { process.env.CCW_DATA_DIR = originalEnv.CCW_DATA_DIR; rmSync(CCW_HOME, { recursive: true, force: true }); rmSync(PROJECT_ROOT, { recursive: true, force: true }); + rmSync(OUTSIDE_ROOT, { recursive: true, force: true }); }); it('GET /api/health returns ok payload', async () => { @@ -237,5 +239,60 @@ describe('system routes integration', async () => { await new Promise((resolve) => originalClose(() => resolve())); } }); -}); + it('GET /api/file reads JSON within initialPath and rejects outside paths', async () => { + const insideFile = join(PROJECT_ROOT, '.review', 'fixes', 'active-fix-session.json'); + mkdirSync(join(PROJECT_ROOT, '.review', 'fixes'), { recursive: true }); + writeFileSync(insideFile, JSON.stringify({ ok: true }), 'utf8'); + + const outsideFile = join(OUTSIDE_ROOT, 'outside.json'); + writeFileSync(outsideFile, JSON.stringify({ ok: true }), 'utf8'); + + const { server, baseUrl } = await createServer(PROJECT_ROOT); + try { + const ok = await requestJson(baseUrl, 'GET', `/api/file?path=${encodeURIComponent(insideFile)}`); + assert.equal(ok.status, 200); + assert.equal(ok.json.ok, true); + + const denied = await requestJson(baseUrl, 'GET', `/api/file?path=${encodeURIComponent(outsideFile)}`); + assert.equal(denied.status, 403); + assert.equal(denied.json.error, 'Access denied'); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); + + it('POST /api/dialog/browse rejects paths outside allowed roots', async () => { + const { server, baseUrl } = await createServer(PROJECT_ROOT); + try { + const rootPath = parse(homedir()).root; + const denied = await requestJson(baseUrl, 'POST', '/api/dialog/browse', { path: rootPath, showHidden: true }); + assert.equal(denied.status, 403); + assert.equal(denied.json.error, 'Access denied'); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); + + it('POST /api/dialog/open-file accepts files under initialPath and rejects outside paths', async () => { + const allowedFile = join(PROJECT_ROOT, 'allowed.txt'); + writeFileSync(allowedFile, 'ok', 'utf8'); + + const rootPath = parse(homedir()).root; + const deniedPath = join(rootPath, 'ccw-not-allowed.txt'); + + const { server, baseUrl } = await createServer(PROJECT_ROOT); + try { + const ok = await requestJson(baseUrl, 'POST', '/api/dialog/open-file', { path: allowedFile }); + assert.equal(ok.status, 200); + assert.equal(ok.json.success, true); + assert.equal(ok.json.isFile, true); + + const denied = await requestJson(baseUrl, 'POST', '/api/dialog/open-file', { path: deniedPath }); + assert.equal(denied.status, 403); + assert.equal(denied.json.error, 'Access denied'); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } + }); +});