From 6e3fbed52ae938ae1b4b6fa131c618f38f20081c Mon Sep 17 00:00:00 2001 From: sagar maurya Date: Sun, 17 May 2026 22:24:41 +0530 Subject: [PATCH] fix: keep disk proxy reads inside proxy root --- lib/utils/resolve-file-proxy.ts | 14 ++++++++++-- tests/routes/file-proxy02.test.ts | 36 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/utils/resolve-file-proxy.ts b/lib/utils/resolve-file-proxy.ts index 3145b4e..e4e6253 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 { isAbsolute, relative, resolve, sep } from "node:path" import { normalizePath } from "./normalize-path" export async function resolveFileProxy( @@ -26,7 +26,17 @@ async function resolveDiskProxy( diskPath: string, relativePath: string, ): Promise { - const fullPath = join(diskPath, relativePath) + const proxyRoot = resolve(diskPath) + const fullPath = resolve(proxyRoot, relativePath) + const pathFromRoot = relative(proxyRoot, fullPath) + + if ( + pathFromRoot === ".." || + pathFromRoot.startsWith(`..${sep}`) || + isAbsolute(pathFromRoot) + ) { + 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..942c5df 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 paths outside proxy root", async () => { + const { axios } = await getTestServer() + + const tempDir = await mkdtemp(join(tmpdir(), "file-proxy-root-test-")) + const proxyRoot = join(tempDir, "public") + + try { + await mkdir(proxyRoot) + await writeFile(join(proxyRoot, "allowed.txt"), "Allowed content") + await writeFile(join(tempDir, "secret.txt"), "Secret content") + + await axios.post("/file_proxies/create", { + proxy_type: "disk", + disk_path: proxyRoot, + matching_pattern: "safe-disk/*", + }) + + const allowedRes = await axios.get("/files/download", { + params: { file_path: "/safe-disk/allowed.txt" }, + }) + expect(allowedRes.status).toBe(200) + expect(allowedRes.data).toBe("Allowed content") + + await expect( + axios.get("/files/download", { + params: { file_path: "/safe-disk/../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()