mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix(openshell): pin host writes to sandbox root (#69797)
* fix(openshell): pin host writes to sandbox root * fix(openshell): use plugin sdk infra runtime * fix(openshell): reject symlink write targets * chore(changelog): note openshell sandbox write fix
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
SandboxResolvedPath,
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import { createWritableRenameTargetResolver } from "openclaw/plugin-sdk/sandbox";
|
||||
import { writeFileWithinRoot } from "openclaw/plugin-sdk/infra-runtime";
|
||||
import type { OpenShellFsBridgeContext, OpenShellSandboxBackend } from "./backend.types.js";
|
||||
import { movePathWithCopyFallback } from "./mirror.js";
|
||||
|
||||
@@ -78,16 +79,12 @@ class OpenShellFsBridge implements SandboxFsBridge {
|
||||
const buffer = Buffer.isBuffer(params.data)
|
||||
? params.data
|
||||
: Buffer.from(params.data, params.encoding ?? "utf8");
|
||||
const parentDir = path.dirname(hostPath);
|
||||
if (params.mkdir !== false) {
|
||||
await fsPromises.mkdir(parentDir, { recursive: true });
|
||||
}
|
||||
const tempPath = path.join(
|
||||
parentDir,
|
||||
`.openclaw-openshell-write-${path.basename(hostPath)}-${process.pid}-${Date.now()}`,
|
||||
);
|
||||
await fsPromises.writeFile(tempPath, buffer);
|
||||
await fsPromises.rename(tempPath, hostPath);
|
||||
await writeFileWithinRoot({
|
||||
rootDir: target.mountHostRoot,
|
||||
relativePath: path.relative(target.mountHostRoot, hostPath),
|
||||
data: buffer,
|
||||
mkdir: params.mkdir,
|
||||
});
|
||||
await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,65 @@ describe("openshell fs bridges", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects symlink-parent writes instead of escaping the local mount root", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const outsideDir = await makeTempDir("openclaw-openshell-outside-");
|
||||
await fs.symlink(outsideDir, path.join(workspaceDir, "alias"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(
|
||||
bridge.writeFile({
|
||||
filePath: "alias/escape.txt",
|
||||
data: "owned",
|
||||
mkdir: true,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
await expect(fs.stat(path.join(outsideDir, "escape.txt"))).rejects.toThrow();
|
||||
await expect(fs.readdir(outsideDir)).resolves.toEqual([]);
|
||||
expect(backend.syncLocalPathToRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects writes whose final target is a symlink inside the local mount root", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const linkedTarget = path.join(workspaceDir, "existing.txt");
|
||||
await fs.writeFile(linkedTarget, "keep", "utf8");
|
||||
await fs.symlink("existing.txt", path.join(workspaceDir, "link.txt"));
|
||||
const backend = createMirrorBackendMock();
|
||||
const sandbox = createSandboxTestContext({
|
||||
overrides: {
|
||||
backendId: "openshell",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
containerWorkdir: "/sandbox",
|
||||
},
|
||||
});
|
||||
|
||||
const { createOpenShellFsBridge } = await import("./fs-bridge.js");
|
||||
const bridge = createOpenShellFsBridge({ sandbox, backend });
|
||||
|
||||
await expect(
|
||||
bridge.writeFile({
|
||||
filePath: "link.txt",
|
||||
data: "owned",
|
||||
mkdir: true,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
await expect(fs.readlink(path.join(workspaceDir, "link.txt"))).resolves.toBe("existing.txt");
|
||||
await expect(fs.readFile(linkedTarget, "utf8")).resolves.toBe("keep");
|
||||
expect(backend.syncLocalPathToRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps agent mount paths when the sandbox workspace is read-only", async () => {
|
||||
const workspaceDir = await makeTempDir("openclaw-openshell-fs-");
|
||||
const agentWorkspaceDir = await makeTempDir("openclaw-openshell-agent-");
|
||||
|
||||
Reference in New Issue
Block a user