fix(desktop): deny shell cwd outside bot computer home
Remove hostRoots from this-mac desktop sandbox so shell execution stays inside the bot computer workspace, matching container path rules. Add resolveDesktopExecuteCwd to map virtual /home/rakazo paths onto the local home and reject host-absolute cwd requests via containment checks. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
ed7ba490b1
commit
a91fc1cb98
@ -1,8 +1,44 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isAllowedDesktopPath, normalizeDesktopWorkspacePath } from "./desktop-sandbox-paths.js";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
isAllowedDesktopPath,
|
||||
normalizeDesktopWorkspacePath,
|
||||
resolveDesktopExecuteCwd,
|
||||
} from "./desktop-sandbox-paths.js";
|
||||
|
||||
describe("desktop sandbox path rules", () => {
|
||||
const home = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-home-"));
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolves shell cwd inside the computer home", () => {
|
||||
expect(resolveDesktopExecuteCwd(undefined, home)).toBe(home);
|
||||
expect(resolveDesktopExecuteCwd(".", home)).toBe(home);
|
||||
expect(resolveDesktopExecuteCwd("/home/rakazo", home)).toBe(home);
|
||||
expect(resolveDesktopExecuteCwd("notes/result.txt", home)).toBe(
|
||||
path.join(home, "notes/result.txt"),
|
||||
);
|
||||
expect(resolveDesktopExecuteCwd("/home/rakazo/notes/result.txt", home)).toBe(
|
||||
path.join(home, "notes/result.txt"),
|
||||
);
|
||||
});
|
||||
|
||||
it("denies host-absolute shell cwd outside the computer home", () => {
|
||||
const outside = path.join(tmpdir(), "outside");
|
||||
const cwd = resolveDesktopExecuteCwd(outside, home);
|
||||
expect(isAllowedDesktopPath(cwd, [home])).toBe(false);
|
||||
expect(isAllowedDesktopPath(resolveDesktopExecuteCwd("/etc", home), [home])).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects traversal in shell cwd requests", () => {
|
||||
expect(() => resolveDesktopExecuteCwd("../outside", home)).toThrow(/escapes/i);
|
||||
expect(() => resolveDesktopExecuteCwd("/home/rakazo/../outside", home)).toThrow(/escapes/i);
|
||||
});
|
||||
|
||||
it("compares Windows roots case-insensitively without accepting siblings or other drives", () => {
|
||||
const roots = ["C:\\Users\\Owner\\Rakazo\\bot"];
|
||||
|
||||
|
||||
@ -10,6 +10,30 @@ export function normalizeDesktopWorkspacePath(value: string, platform = process.
|
||||
return normalizeWorkspacePath(value);
|
||||
}
|
||||
|
||||
/** Map shell cwd requests onto the desktop computer home, matching container rules. */
|
||||
export function resolveDesktopExecuteCwd(requestCwd: string | undefined, home: string): string {
|
||||
if (
|
||||
!requestCwd ||
|
||||
requestCwd === "." ||
|
||||
requestCwd === "/" ||
|
||||
requestCwd === "/home/rakazo" ||
|
||||
requestCwd === "/home/user"
|
||||
) {
|
||||
return home;
|
||||
}
|
||||
if (requestCwd.startsWith("/home/rakazo/")) {
|
||||
return path.join(
|
||||
home,
|
||||
normalizeDesktopWorkspacePath(requestCwd.slice("/home/rakazo/".length)),
|
||||
);
|
||||
}
|
||||
if (path.isAbsolute(requestCwd)) {
|
||||
// Host-absolute paths must not be rebased into the workspace; containment rejects them.
|
||||
return path.resolve(requestCwd);
|
||||
}
|
||||
return path.join(home, normalizeDesktopWorkspacePath(requestCwd));
|
||||
}
|
||||
|
||||
export function isAllowedDesktopPath(
|
||||
target: string,
|
||||
roots: string[],
|
||||
|
||||
@ -35,7 +35,11 @@ import {
|
||||
normalizeWorkspacePath,
|
||||
placeholderObservation,
|
||||
} from "./computer-support.js";
|
||||
import { isAllowedDesktopPath, normalizeDesktopWorkspacePath } from "./desktop-sandbox-paths.js";
|
||||
import {
|
||||
isAllowedDesktopPath,
|
||||
normalizeDesktopWorkspacePath,
|
||||
resolveDesktopExecuteCwd,
|
||||
} from "./desktop-sandbox-paths.js";
|
||||
import {
|
||||
createExclusiveChildViaDirectoryFdWin32,
|
||||
mkdirChildViaDirectoryFdWin32,
|
||||
@ -61,7 +65,7 @@ interface DesktopBox {
|
||||
export class DesktopSandboxProvider implements SandboxProvider {
|
||||
readonly boxes = new Map<string, DesktopBox>();
|
||||
|
||||
constructor(private readonly opts: { root?: string; hostRoots?: string[] } = {}) {}
|
||||
constructor(private readonly opts: { root?: string } = {}) {}
|
||||
|
||||
describe() {
|
||||
return {
|
||||
@ -128,8 +132,8 @@ export class DesktopSandboxProvider implements SandboxProvider {
|
||||
yield { type: "exit", code: 1 };
|
||||
return;
|
||||
}
|
||||
const cwd = resolveExecuteCwd(request.cwd, box.home);
|
||||
if (!isAllowedDesktopPath(cwd, this.allowedRoots(box.home))) {
|
||||
const cwd = resolveDesktopExecuteCwd(request.cwd, box.home);
|
||||
if (!isAllowedDesktopPath(cwd, [box.home])) {
|
||||
yield { type: "stderr", data: "path is outside this computer's home" };
|
||||
yield { type: "exit", code: 1 };
|
||||
return;
|
||||
@ -302,9 +306,6 @@ export class DesktopSandboxProvider implements SandboxProvider {
|
||||
return box;
|
||||
}
|
||||
|
||||
private allowedRoots(home: string) {
|
||||
return [home, ...(this.opts.hostRoots ?? [])];
|
||||
}
|
||||
}
|
||||
|
||||
async function localWorkspaceTarget(home: string, relative: string, mustExist: boolean) {
|
||||
@ -683,11 +684,6 @@ async function* walkDesktopWorkspace(home: string, directory: string): AsyncIter
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExecuteCwd(requestCwd: string | undefined, home: string) {
|
||||
if (!requestCwd || requestCwd === "/home/rakazo" || requestCwd === "/home/user") return home;
|
||||
return path.resolve(home, requestCwd);
|
||||
}
|
||||
|
||||
function runCommand(
|
||||
argv: string[],
|
||||
cwd: string,
|
||||
|
||||
@ -70,23 +70,26 @@ describe("host-aware sandbox", () => {
|
||||
expect(sandbox.pageBrowser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets this-mac cwd run under a host root", async () => {
|
||||
const desktop = new DesktopSandboxProvider({ hostRoots: [hostRoot] });
|
||||
it("refuses shell cwd outside the bot computer home", async () => {
|
||||
const desktop = new DesktopSandboxProvider();
|
||||
const computer = await desktop.provision({ botId: "host", homePath: "/tmp/host-home" }, ctx);
|
||||
let code = 1;
|
||||
let stderr = "";
|
||||
let code = 0;
|
||||
for await (const event of desktop.execute(
|
||||
computer,
|
||||
{ argv: ["echo", "ok"], cwd: hostRoot },
|
||||
{ argv: ["echo", "nope"], cwd: hostRoot },
|
||||
ctx,
|
||||
)) {
|
||||
if (event.type === "stderr") stderr += event.data;
|
||||
if (event.type === "exit") code = event.code;
|
||||
}
|
||||
expect(code).toBe(0);
|
||||
expect(code).toBe(1);
|
||||
expect(stderr).toMatch(/outside this computer's home/i);
|
||||
await desktop.destroy(computer, ctx);
|
||||
});
|
||||
|
||||
it("still refuses paths outside home and host roots", async () => {
|
||||
const desktop = new DesktopSandboxProvider({ hostRoots: [hostRoot] });
|
||||
it("still refuses paths outside the bot computer home", async () => {
|
||||
const desktop = new DesktopSandboxProvider();
|
||||
const computer = await desktop.provision({ botId: "deny", homePath: "/tmp/deny" }, ctx);
|
||||
let stderr = "";
|
||||
let code = 0;
|
||||
@ -103,6 +106,21 @@ describe("host-aware sandbox", () => {
|
||||
await desktop.destroy(computer, ctx);
|
||||
});
|
||||
|
||||
it("keeps shell cwd inside the bot computer home", async () => {
|
||||
const desktop = new DesktopSandboxProvider();
|
||||
const computer = await desktop.provision({ botId: "inside", homePath: "/tmp/inside" }, ctx);
|
||||
let code = 1;
|
||||
for await (const event of desktop.execute(
|
||||
computer,
|
||||
{ argv: ["echo", "ok"], cwd: "projects/site" },
|
||||
ctx,
|
||||
)) {
|
||||
if (event.type === "exit") code = event.code;
|
||||
}
|
||||
expect(code).toBe(0);
|
||||
await desktop.destroy(computer, ctx);
|
||||
});
|
||||
|
||||
it("provisions on the host provider when enabled", async () => {
|
||||
const isolated = new FakeSandboxProvider();
|
||||
const host = new DesktopSandboxProvider();
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { homedir } from "node:os";
|
||||
import type {
|
||||
AdapterContext,
|
||||
CommandRequest,
|
||||
@ -25,19 +24,13 @@ export function createRunSandbox(
|
||||
opts: SandboxProviderOptions & { prisma?: PrismaClient },
|
||||
): SandboxProvider {
|
||||
if (kind === "desktop") {
|
||||
return new DesktopSandboxProvider({
|
||||
root: opts.dataDir,
|
||||
hostRoots: [homedir()],
|
||||
});
|
||||
return new DesktopSandboxProvider({ root: opts.dataDir });
|
||||
}
|
||||
const primary = createSandboxProvider(kind, opts);
|
||||
if (kind !== "docker" || !opts.prisma) return primary;
|
||||
return new HostAwareSandbox(
|
||||
primary,
|
||||
new DesktopSandboxProvider({
|
||||
root: opts.dataDir,
|
||||
hostRoots: [homedir()],
|
||||
}),
|
||||
new DesktopSandboxProvider({ root: opts.dataDir }),
|
||||
async () => {
|
||||
const settings = await opts.prisma!.deploymentSettings.findUnique({
|
||||
where: { id: "default" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user