diff --git a/packages/server/src/server/file-explorer/service.test.ts b/packages/server/src/server/file-explorer/service.test.ts index e1549e193..b9076a56c 100644 --- a/packages/server/src/server/file-explorer/service.test.ts +++ b/packages/server/src/server/file-explorer/service.test.ts @@ -4,6 +4,10 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { listDirectoryEntries, readExplorerFile } from "./service.js"; +async function createHomeTempDir(prefix: string): Promise { + return mkdtemp(path.join(os.homedir(), prefix)); +} + async function createTempDir(prefix: string): Promise { return mkdtemp(path.join(os.tmpdir(), prefix)); } @@ -97,6 +101,41 @@ describe("file explorer service", () => { } }); + it("expands a ~ prefix in relative paths against the user home directory", async () => { + const root = await createHomeTempDir(".paseo-file-explorer-home-"); + + try { + const filePath = path.join(root, "sample.txt"); + await writeFile(filePath, "hello from home\n", "utf-8"); + + const tildePath = `~/${path.relative(os.homedir(), filePath)}`; + const result = await readExplorerFile({ + root, + relativePath: tildePath, + }); + + expect(result.kind).toBe("text"); + expect(result.content).toBe("hello from home\n"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("rejects ~-prefixed paths that resolve outside the workspace", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "paseo-file-explorer-outside-home-")); + + try { + await expect( + readExplorerFile({ + root, + relativePath: "~/some/file.txt", + }), + ).rejects.toThrow("Access outside of workspace is not allowed"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it("rejects symlinked files that resolve outside the workspace", async () => { const root = await createTempDir("paseo-file-explorer-"); const outsideRoot = await createTempDir("paseo-file-explorer-outside-"); diff --git a/packages/server/src/server/file-explorer/service.ts b/packages/server/src/server/file-explorer/service.ts index 50f139d74..27d975d03 100644 --- a/packages/server/src/server/file-explorer/service.ts +++ b/packages/server/src/server/file-explorer/service.ts @@ -1,5 +1,6 @@ import { promises as fs } from "fs"; import path from "path"; +import { resolvePathFromBase } from "../path-utils.js"; export type ExplorerEntryKind = "file" | "directory"; export type ExplorerFileKind = "text" | "image" | "binary"; @@ -207,7 +208,7 @@ export async function getDownloadableFileInfo({ root, relativePath }: ReadFilePa async function resolveScopedPath({ root, relativePath = "." }: ScopedPathParams): Promise { const normalizedRoot = path.resolve(root); - const requestedPath = path.resolve(normalizedRoot, relativePath); + const requestedPath = resolvePathFromBase(normalizedRoot, relativePath); const relative = path.relative(normalizedRoot, requestedPath); if (relative !== "" && (relative.startsWith("..") || path.isAbsolute(relative))) {