fix: enforce apply_patch workspaceOnly in sandbox mounts

This commit is contained in:
Peter Steinberger
2026-02-24 02:23:30 +00:00
parent c070be1bc4
commit 6634030be3
3 changed files with 83 additions and 0 deletions

View File

@@ -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,

View File

@@ -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",
);
});
});
});