mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:44:31 +00:00
test(browser): dedupe fixture lifecycle and cover directory-path rejection
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveExistingPathsWithinRoot } from "./paths.js";
|
import { resolveExistingPathsWithinRoot } from "./paths.js";
|
||||||
|
|
||||||
async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> {
|
async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> {
|
||||||
@@ -11,88 +11,79 @@ async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: strin
|
|||||||
return { baseDir, uploadsDir };
|
return { baseDir, uploadsDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function withFixtureRoot<T>(
|
||||||
|
run: (ctx: { baseDir: string; uploadsDir: string }) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const fixture = await createFixtureRoot();
|
||||||
|
try {
|
||||||
|
return await run(fixture);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(fixture.baseDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("resolveExistingPathsWithinRoot", () => {
|
describe("resolveExistingPathsWithinRoot", () => {
|
||||||
const cleanupDirs = new Set<string>();
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(cleanupDirs).map(async (dir) => {
|
|
||||||
await fs.rm(dir, { recursive: true, force: true });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
cleanupDirs.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts existing files under the upload root", async () => {
|
it("accepts existing files under the upload root", async () => {
|
||||||
const { baseDir, uploadsDir } = await createFixtureRoot();
|
await withFixtureRoot(async ({ uploadsDir }) => {
|
||||||
cleanupDirs.add(baseDir);
|
const nestedDir = path.join(uploadsDir, "nested");
|
||||||
|
await fs.mkdir(nestedDir, { recursive: true });
|
||||||
const nestedDir = path.join(uploadsDir, "nested");
|
const filePath = path.join(nestedDir, "ok.txt");
|
||||||
await fs.mkdir(nestedDir, { recursive: true });
|
await fs.writeFile(filePath, "ok", "utf8");
|
||||||
const filePath = path.join(nestedDir, "ok.txt");
|
|
||||||
await fs.writeFile(filePath, "ok", "utf8");
|
|
||||||
|
|
||||||
const result = await resolveExistingPathsWithinRoot({
|
|
||||||
rootDir: uploadsDir,
|
|
||||||
requestedPaths: [filePath],
|
|
||||||
scopeLabel: "uploads directory",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
if (result.ok) {
|
|
||||||
expect(result.paths).toEqual([await fs.realpath(filePath)]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects traversal outside the upload root", async () => {
|
|
||||||
const { baseDir, uploadsDir } = await createFixtureRoot();
|
|
||||||
cleanupDirs.add(baseDir);
|
|
||||||
|
|
||||||
const outsidePath = path.join(baseDir, "outside.txt");
|
|
||||||
await fs.writeFile(outsidePath, "nope", "utf8");
|
|
||||||
|
|
||||||
const result = await resolveExistingPathsWithinRoot({
|
|
||||||
rootDir: uploadsDir,
|
|
||||||
requestedPaths: ["../outside.txt"],
|
|
||||||
scopeLabel: "uploads directory",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(false);
|
|
||||||
if (!result.ok) {
|
|
||||||
expect(result.error).toContain("must stay within uploads directory");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps lexical in-root paths when files do not exist yet", async () => {
|
|
||||||
const { baseDir, uploadsDir } = await createFixtureRoot();
|
|
||||||
cleanupDirs.add(baseDir);
|
|
||||||
|
|
||||||
const result = await resolveExistingPathsWithinRoot({
|
|
||||||
rootDir: uploadsDir,
|
|
||||||
requestedPaths: ["missing.txt"],
|
|
||||||
scopeLabel: "uploads directory",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
|
||||||
if (result.ok) {
|
|
||||||
expect(result.paths).toEqual([path.join(uploadsDir, "missing.txt")]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it.runIf(process.platform !== "win32")(
|
|
||||||
"rejects symlink escapes outside upload root",
|
|
||||||
async () => {
|
|
||||||
const { baseDir, uploadsDir } = await createFixtureRoot();
|
|
||||||
cleanupDirs.add(baseDir);
|
|
||||||
|
|
||||||
const outsidePath = path.join(baseDir, "secret.txt");
|
|
||||||
await fs.writeFile(outsidePath, "secret", "utf8");
|
|
||||||
const symlinkPath = path.join(uploadsDir, "leak.txt");
|
|
||||||
await fs.symlink(outsidePath, symlinkPath);
|
|
||||||
|
|
||||||
const result = await resolveExistingPathsWithinRoot({
|
const result = await resolveExistingPathsWithinRoot({
|
||||||
rootDir: uploadsDir,
|
rootDir: uploadsDir,
|
||||||
requestedPaths: ["leak.txt"],
|
requestedPaths: [filePath],
|
||||||
|
scopeLabel: "uploads directory",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.paths).toEqual([await fs.realpath(filePath)]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects traversal outside the upload root", async () => {
|
||||||
|
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
|
||||||
|
const outsidePath = path.join(baseDir, "outside.txt");
|
||||||
|
await fs.writeFile(outsidePath, "nope", "utf8");
|
||||||
|
|
||||||
|
const result = await resolveExistingPathsWithinRoot({
|
||||||
|
rootDir: uploadsDir,
|
||||||
|
requestedPaths: ["../outside.txt"],
|
||||||
|
scopeLabel: "uploads directory",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toContain("must stay within uploads directory");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps lexical in-root paths when files do not exist yet", async () => {
|
||||||
|
await withFixtureRoot(async ({ uploadsDir }) => {
|
||||||
|
const result = await resolveExistingPathsWithinRoot({
|
||||||
|
rootDir: uploadsDir,
|
||||||
|
requestedPaths: ["missing.txt"],
|
||||||
|
scopeLabel: "uploads directory",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.paths).toEqual([path.join(uploadsDir, "missing.txt")]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects directory paths inside upload root", async () => {
|
||||||
|
await withFixtureRoot(async ({ uploadsDir }) => {
|
||||||
|
const nestedDir = path.join(uploadsDir, "nested");
|
||||||
|
await fs.mkdir(nestedDir, { recursive: true });
|
||||||
|
|
||||||
|
const result = await resolveExistingPathsWithinRoot({
|
||||||
|
rootDir: uploadsDir,
|
||||||
|
requestedPaths: ["nested"],
|
||||||
scopeLabel: "uploads directory",
|
scopeLabel: "uploads directory",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,6 +91,29 @@ describe("resolveExistingPathsWithinRoot", () => {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
expect(result.error).toContain("regular non-symlink file");
|
expect(result.error).toContain("regular non-symlink file");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.runIf(process.platform !== "win32")(
|
||||||
|
"rejects symlink escapes outside upload root",
|
||||||
|
async () => {
|
||||||
|
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
|
||||||
|
const outsidePath = path.join(baseDir, "secret.txt");
|
||||||
|
await fs.writeFile(outsidePath, "secret", "utf8");
|
||||||
|
const symlinkPath = path.join(uploadsDir, "leak.txt");
|
||||||
|
await fs.symlink(outsidePath, symlinkPath);
|
||||||
|
|
||||||
|
const result = await resolveExistingPathsWithinRoot({
|
||||||
|
rootDir: uploadsDir,
|
||||||
|
requestedPaths: ["leak.txt"],
|
||||||
|
scopeLabel: "uploads directory",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toContain("regular non-symlink file");
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user