mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 04:24:31 +00:00
fix(security): harden browser trace/download temp path handling
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fetch as realFetch } from "undici";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
|
||||
import { DEFAULT_DOWNLOAD_DIR, DEFAULT_TRACE_DIR, DEFAULT_UPLOAD_DIR } from "./paths.js";
|
||||
import {
|
||||
installAgentContractHooks,
|
||||
postJson,
|
||||
@@ -16,6 +18,23 @@ import {
|
||||
const state = getBrowserControlServerTestState();
|
||||
const pwMocks = getPwMocks();
|
||||
|
||||
async function withSymlinkPathEscape<T>(params: {
|
||||
rootDir: string;
|
||||
run: (relativePath: string) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-route-escape-"));
|
||||
const linkName = `escape-link-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const linkPath = path.join(params.rootDir, linkName);
|
||||
await fs.mkdir(params.rootDir, { recursive: true });
|
||||
await fs.symlink(outsideDir, linkPath);
|
||||
try {
|
||||
return await params.run(`${linkName}/pwned.zip`);
|
||||
} finally {
|
||||
await fs.unlink(linkPath).catch(() => {});
|
||||
await fs.rm(outsideDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
describe("browser control server", () => {
|
||||
installAgentContractHooks();
|
||||
|
||||
@@ -268,6 +287,58 @@ describe("browser control server", () => {
|
||||
expect(pwMocks.downloadViaPlaywright).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"trace stop rejects symlinked write path escape under trace dir",
|
||||
async () => {
|
||||
const base = await startServerAndBase();
|
||||
await withSymlinkPathEscape({
|
||||
rootDir: DEFAULT_TRACE_DIR,
|
||||
run: async (pathEscape) => {
|
||||
const res = await postJson<{ error?: string }>(`${base}/trace/stop`, {
|
||||
path: pathEscape,
|
||||
});
|
||||
expect(res.error).toContain("Invalid path");
|
||||
expect(pwMocks.traceStopViaPlaywright).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"wait/download rejects symlinked write path escape under downloads dir",
|
||||
async () => {
|
||||
const base = await startServerAndBase();
|
||||
await withSymlinkPathEscape({
|
||||
rootDir: DEFAULT_DOWNLOAD_DIR,
|
||||
run: async (pathEscape) => {
|
||||
const res = await postJson<{ error?: string }>(`${base}/wait/download`, {
|
||||
path: pathEscape,
|
||||
});
|
||||
expect(res.error).toContain("Invalid path");
|
||||
expect(pwMocks.waitForDownloadViaPlaywright).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"download rejects symlinked write path escape under downloads dir",
|
||||
async () => {
|
||||
const base = await startServerAndBase();
|
||||
await withSymlinkPathEscape({
|
||||
rootDir: DEFAULT_DOWNLOAD_DIR,
|
||||
run: async (pathEscape) => {
|
||||
const res = await postJson<{ error?: string }>(`${base}/download`, {
|
||||
ref: "e12",
|
||||
path: pathEscape,
|
||||
});
|
||||
expect(res.error).toContain("Invalid path");
|
||||
expect(pwMocks.downloadViaPlaywright).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("wait/download accepts in-root relative output path", async () => {
|
||||
const base = await startServerAndBase();
|
||||
const res = await postJson<{ ok?: boolean; download?: { path?: string } }>(
|
||||
|
||||
Reference in New Issue
Block a user