From 6c45931fc67aa6f723e1b89e631bdcc97112ee25 Mon Sep 17 00:00:00 2001 From: JianJroh Date: Tue, 19 May 2026 11:39:55 +0800 Subject: [PATCH] fix(core): resolve open-in-editor/finder paths against vite cwd --- .../src/node/__tests__/open-in-editor.test.ts | 21 +++++++++++++++++-- .../src/node/rpc/public/open-in-editor.ts | 2 +- .../src/node/rpc/public/open-in-finder.ts | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/core/src/node/__tests__/open-in-editor.test.ts b/packages/core/src/node/__tests__/open-in-editor.test.ts index cc6cda53..fc98405d 100644 --- a/packages/core/src/node/__tests__/open-in-editor.test.ts +++ b/packages/core/src/node/__tests__/open-in-editor.test.ts @@ -1,6 +1,7 @@ import type { DevToolsNodeContext } from '@vitejs/devtools-kit' import { resolve } from 'node:path' -import { describe, expect, it, vi } from 'vitest' +import { launchEditor } from 'devframe/utils/launch-editor' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { openInEditor } from '../rpc/public/open-in-editor' // Mock launch-editor so tests don't actually open files @@ -20,6 +21,10 @@ describe('openInEditor – path traversal protection', () => { return handler as (path: string) => Promise } + beforeEach(() => { + vi.mocked(launchEditor).mockClear() + }) + it('allows opening a file inside the project root', async () => { const handler = await getHandler() await expect(handler('src/main.ts')).resolves.not.toThrow() @@ -30,6 +35,18 @@ describe('openInEditor – path traversal protection', () => { await expect(handler('src/utils/helper.ts')).resolves.not.toThrow() }) + it('resolves relative paths against cwd (Vite project root), not workspaceRoot', async () => { + const handler = await getHandler() + await handler('src/main.ts') + expect(launchEditor).toHaveBeenCalledWith(resolve(cwd, 'src/main.ts')) + }) + + it('allows jumping to sibling packages within the workspace root', async () => { + const handler = await getHandler() + await expect(handler('../sibling-pkg/src/foo.ts')).resolves.not.toThrow() + expect(launchEditor).toHaveBeenCalledWith(resolve(workspaceRoot, 'sibling-pkg/src/foo.ts')) + }) + it('rejects path traversal with ../', async () => { const handler = await getHandler() await expect(handler('../../etc/passwd')).rejects.toThrow( @@ -46,7 +63,7 @@ describe('openInEditor – path traversal protection', () => { it('rejects traversal disguised within a subpath', async () => { const handler = await getHandler() - await expect(handler('src/../../secret/file.txt')).rejects.toThrow( + await expect(handler('src/../../../secret/file.txt')).rejects.toThrow( 'Path is outside the workspace root', ) }) diff --git a/packages/core/src/node/rpc/public/open-in-editor.ts b/packages/core/src/node/rpc/public/open-in-editor.ts index d728f785..9ff73876 100644 --- a/packages/core/src/node/rpc/public/open-in-editor.ts +++ b/packages/core/src/node/rpc/public/open-in-editor.ts @@ -10,7 +10,7 @@ export const openInEditor = defineRpcFunction({ setup: (context) => { return { handler: async (path: string) => { - const resolved = resolve(context.workspaceRoot, path) + const resolved = resolve(context.cwd, path) const rel = relative(context.workspaceRoot, resolved) // Prevent escaping the workspace root diff --git a/packages/core/src/node/rpc/public/open-in-finder.ts b/packages/core/src/node/rpc/public/open-in-finder.ts index 7ad4ce91..27651029 100644 --- a/packages/core/src/node/rpc/public/open-in-finder.ts +++ b/packages/core/src/node/rpc/public/open-in-finder.ts @@ -10,7 +10,7 @@ export const openInFinder = defineRpcFunction({ setup: (context) => { return { handler: async (path: string) => { - const resolved = resolve(context.workspaceRoot, path) + const resolved = resolve(context.cwd, path) const rel = relative(context.workspaceRoot, resolved) // Ensure the path stays within workspace root