diff --git a/app/server/modules/volumes/__tests__/volumes.service.test.ts b/app/server/modules/volumes/__tests__/volumes.service.test.ts new file mode 100644 index 00000000..d852e768 --- /dev/null +++ b/app/server/modules/volumes/__tests__/volumes.service.test.ts @@ -0,0 +1,48 @@ +import { test, describe, expect } from "bun:test"; +import { volumeService } from "../volume.service"; +import { db } from "~/server/db/db"; +import { volumesTable } from "~/server/db/schema"; +import { randomUUID } from "node:crypto"; +import * as fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { createTestSession } from "~/test/helpers/auth"; +import { withContext } from "~/server/core/request-context"; + +describe("volumeService security", () => { + describe("path traversal", () => { + test("should reject traversal outside the volume root in listFiles", async () => { + const { organizationId, user } = await createTestSession(); + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-vol-svc-")); + const volumePath = path.join(tempRoot, "vol"); + const secretPath = path.join(tempRoot, "volume-secret"); + + await fs.mkdir(volumePath, { recursive: true }); + await fs.mkdir(secretPath, { recursive: true }); + await fs.writeFile(path.join(secretPath, "secret.txt"), "top secret", "utf-8"); + + const [volume] = await db + .insert(volumesTable) + .values({ + shortId: randomUUID().slice(0, 8), + name: `test-vol-${randomUUID().slice(0, 8)}`, + type: "directory", + status: "mounted", + config: { backend: "directory", path: volumePath }, + autoRemount: true, + organizationId, + }) + .returning(); + + try { + await withContext({ organizationId, userId: user.id }, async () => { + const traversalPath = `../${path.basename(secretPath)}`; + + expect(volumeService.listFiles(volume.id, traversalPath)).rejects.toThrow("Invalid path"); + }); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + }); +}); diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 70c0f0d9..d76a9cea 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -329,7 +329,9 @@ const listFiles = async (idOrShortId: string | number, subPath?: string) => { const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath; const normalizedPath = path.normalize(requestedPath); - if (!normalizedPath.startsWith(volumePath)) { + const relative = path.relative(volumePath, normalizedPath); + + if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new InternalServerError("Invalid path"); }