fix: prevent path traversal in list volume files
This commit is contained in:
parent
ffc43e0c25
commit
88fa06b503
2 changed files with 51 additions and 1 deletions
48
app/server/modules/volumes/__tests__/volumes.service.test.ts
Normal file
48
app/server/modules/volumes/__tests__/volumes.service.test.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -329,7 +329,9 @@ const listFiles = async (idOrShortId: string | number, subPath?: string) => {
|
||||||
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
|
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
|
||||||
|
|
||||||
const normalizedPath = path.normalize(requestedPath);
|
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");
|
throw new InternalServerError("Invalid path");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue