diff --git a/lib/utils/resolve-file-proxy.ts b/lib/utils/resolve-file-proxy.ts index 3145b4e..5743f4d 100644 --- a/lib/utils/resolve-file-proxy.ts +++ b/lib/utils/resolve-file-proxy.ts @@ -1,6 +1,6 @@ import type { FileProxy } from "../db/schema" import { readFile } from "node:fs/promises" -import { join } from "node:path" +import { relative, resolve, sep } from "node:path" import { normalizePath } from "./normalize-path" export async function resolveFileProxy( @@ -26,7 +26,13 @@ async function resolveDiskProxy( diskPath: string, relativePath: string, ): Promise { - const fullPath = join(diskPath, relativePath) + const rootPath = resolve(diskPath) + const fullPath = resolve(rootPath, relativePath) + const pathFromRoot = relative(rootPath, fullPath) + + if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${sep}`)) { + return new Response("File not found", { status: 404 }) + } try { const content = await readFile(fullPath) diff --git a/tests/routes/file-proxy02.test.ts b/tests/routes/file-proxy02.test.ts index c1314e8..2aba85f 100644 --- a/tests/routes/file-proxy02.test.ts +++ b/tests/routes/file-proxy02.test.ts @@ -91,6 +91,42 @@ test("disk proxy with query param download", async () => { } }) +test("disk proxy blocks parent directory traversal", async () => { + const { axios } = await getTestServer() + + const tempDir = await mkdtemp(join(tmpdir(), "file-proxy-traversal-")) + const proxyRoot = join(tempDir, "root") + + try { + await mkdir(proxyRoot) + await writeFile(join(proxyRoot, "safe.txt"), "Safe proxy content") + await writeFile(join(tempDir, "secret.txt"), "Secret sibling content") + + await axios.post("/file_proxies/create", { + proxy_type: "disk", + disk_path: proxyRoot, + matching_pattern: "traversal/*", + }) + + const safeRes = await axios.get("/files/download", { + params: { file_path: "/traversal/safe.txt" }, + }) + expect(safeRes.status).toBe(200) + expect(safeRes.data).toBe("Safe proxy content") + + await expect( + axios.get("/files/download", { + params: { file_path: "/traversal/../secret.txt" }, + }), + ).rejects.toMatchObject({ + status: 404, + data: "File not found", + }) + } finally { + await rm(tempDir, { recursive: true, force: true }) + } +}) + test("disk proxy binary file", async () => { const { axios } = await getTestServer()