mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:11:26 +00:00
fix: enforce apply_patch workspaceOnly in sandbox mounts
This commit is contained in:
@@ -260,6 +260,14 @@ async function resolvePatchPath(
|
||||
filePath,
|
||||
cwd: options.cwd,
|
||||
});
|
||||
if (options.workspaceOnly !== false) {
|
||||
await assertSandboxPath({
|
||||
filePath: resolved.hostPath,
|
||||
cwd: options.cwd,
|
||||
root: options.cwd,
|
||||
allowFinalSymlink: purpose === "unlink",
|
||||
});
|
||||
}
|
||||
return {
|
||||
resolved: resolved.hostPath,
|
||||
display: resolved.relativePath || resolved.hostPath,
|
||||
|
||||
@@ -73,6 +73,10 @@ function createSandbox(params: {
|
||||
});
|
||||
}
|
||||
|
||||
type ToolWithExecute = {
|
||||
execute: (toolCallId: string, args: unknown, signal?: AbortSignal) => Promise<unknown>;
|
||||
};
|
||||
|
||||
async function withUnsafeMountedSandboxHarness(
|
||||
run: (ctx: { sandboxRoot: string; agentRoot: string; sandbox: SandboxContext }) => Promise<void>,
|
||||
) {
|
||||
@@ -131,4 +135,74 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
expect(await fs.readFile(path.join(agentRoot, "secret.txt"), "utf8")).toBe("shh");
|
||||
});
|
||||
});
|
||||
|
||||
it("enforces apply_patch workspace-only in sandbox mounts by default", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
allow: ["read", "exec"],
|
||||
exec: { applyPatch: { enabled: true } },
|
||||
},
|
||||
};
|
||||
const tools = createOpenClawCodingTools({
|
||||
sandbox,
|
||||
workspaceDir: sandboxRoot,
|
||||
config: cfg,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-5.2",
|
||||
});
|
||||
const applyPatchTool = tools.find((t) => t.name === "apply_patch") as
|
||||
| ToolWithExecute
|
||||
| undefined;
|
||||
if (!applyPatchTool) {
|
||||
throw new Error("apply_patch tool missing");
|
||||
}
|
||||
|
||||
const patch = `*** Begin Patch
|
||||
*** Add File: /agent/pwned.txt
|
||||
+owned-by-apply-patch
|
||||
*** End Patch`;
|
||||
|
||||
await expect(applyPatchTool.execute("t1", { input: patch })).rejects.toThrow(
|
||||
/Path escapes sandbox root/i,
|
||||
);
|
||||
await expect(fs.stat(path.join(agentRoot, "pwned.txt"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("allows apply_patch outside workspace root when explicitly disabled", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
allow: ["read", "exec"],
|
||||
exec: { applyPatch: { enabled: true, workspaceOnly: false } },
|
||||
},
|
||||
};
|
||||
const tools = createOpenClawCodingTools({
|
||||
sandbox,
|
||||
workspaceDir: sandboxRoot,
|
||||
config: cfg,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-5.2",
|
||||
});
|
||||
const applyPatchTool = tools.find((t) => t.name === "apply_patch") as
|
||||
| ToolWithExecute
|
||||
| undefined;
|
||||
if (!applyPatchTool) {
|
||||
throw new Error("apply_patch tool missing");
|
||||
}
|
||||
|
||||
const patch = `*** Begin Patch
|
||||
*** Add File: /agent/pwned.txt
|
||||
+owned-by-apply-patch
|
||||
*** End Patch`;
|
||||
|
||||
await applyPatchTool.execute("t2", { input: patch });
|
||||
expect(await fs.readFile(path.join(agentRoot, "pwned.txt"), "utf8")).toBe(
|
||||
"owned-by-apply-patch\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user