fix(sandbox): anchor fs-bridge mkdirp
This commit is contained in:
parent
a505be78ab
commit
09cfcf9dd5
@ -738,7 +738,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Slack/download-file scoping: thread/channel-aware `download-file` actions now propagate optional scope context and reject downloads when Slack metadata definitively shows the file is outside the requested channel/thread, while preserving legacy behavior when share metadata is unavailable.
|
- Slack/download-file scoping: thread/channel-aware `download-file` actions now propagate optional scope context and reject downloads when Slack metadata definitively shows the file is outside the requested channel/thread, while preserving legacy behavior when share metadata is unavailable.
|
||||||
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
- Security/Sandbox media staging: block destination symlink escapes in `stageSandboxMedia` by replacing direct destination copies with root-scoped safe writes for both local and SCP-staged attachments, preventing out-of-workspace file overwrite through `media/inbound` alias traversal. This ships in the next npm release (`2026.3.2`). Thanks @tdjackey for reporting.
|
- Security/Sandbox media staging: block destination symlink escapes in `stageSandboxMedia` by replacing direct destination copies with root-scoped safe writes for both local and SCP-staged attachments, preventing out-of-workspace file overwrite through `media/inbound` alias traversal. This ships in the next npm release (`2026.3.2`). Thanks @tdjackey for reporting.
|
||||||
- Security/Sandbox fs bridge: harden sandbox `remove` and `rename` operations by anchoring destructive actions to verified canonical parent directories plus basenames instead of passing mutable full path strings to `rm` and `mv`, reducing parent-directory symlink-rebind TOCTOU exposure in sandbox file operations. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Sandbox fs bridge: harden sandbox `mkdirp`, `remove`, and `rename` operations by anchoring filesystem changes to verified canonical parent directories plus basenames instead of passing mutable full path strings to `mkdir -p`, `rm`, and `mv`, reducing parent-directory symlink-rebind TOCTOU exposure in sandbox file operations. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
- Node host/service auth env: include `OPENCLAW_GATEWAY_TOKEN` in `openclaw node install` service environments (with `CLAWDBOT_GATEWAY_TOKEN` compatibility fallback) so installed node services keep remote gateway token auth across restart/reboot. Fixes #31041. Thanks @OneStepAt4time for reporting, @byungsker, @liuxiaopai-ai, and @vincentkoc.
|
- Node host/service auth env: include `OPENCLAW_GATEWAY_TOKEN` in `openclaw node install` service environments (with `CLAWDBOT_GATEWAY_TOKEN` compatibility fallback) so installed node services keep remote gateway token auth across restart/reboot. Fixes #31041. Thanks @OneStepAt4time for reporting, @byungsker, @liuxiaopai-ai, and @vincentkoc.
|
||||||
- Security/Subagents sandbox inheritance: block sandboxed sessions from spawning cross-agent subagents that would run unsandboxed, preventing runtime sandbox downgrade via `sessions_spawn agentId`. Thanks @tdjackey for reporting.
|
- Security/Subagents sandbox inheritance: block sandboxed sessions from spawning cross-agent subagents that would run unsandboxed, preventing runtime sandbox downgrade via `sessions_spawn agentId`. Thanks @tdjackey for reporting.
|
||||||
- Security/Workspace safe writes: harden `writeFileWithinRoot` against symlink-retarget TOCTOU races by opening existing files without truncation, creating missing files with exclusive create, deferring truncation until post-open identity+boundary validation, and removing out-of-root create artifacts on blocked races; added regression tests for truncate/create race paths. This ships in the next npm release (`2026.3.2`). Thanks @tdjackey for reporting.
|
- Security/Workspace safe writes: harden `writeFileWithinRoot` against symlink-retarget TOCTOU races by opening existing files without truncation, creating missing files with exclusive create, deferring truncation until post-open identity+boundary validation, and removing out-of-root create artifacts on blocked races; added regression tests for truncate/create race paths. This ships in the next npm release (`2026.3.2`). Thanks @tdjackey for reporting.
|
||||||
|
|||||||
@ -135,10 +135,12 @@ async function expectMkdirpAllowsExistingDirectory(params?: { forceBoundaryIoFal
|
|||||||
|
|
||||||
await expect(bridge.mkdirp({ filePath: "memory/kemik" })).resolves.toBeUndefined();
|
await expect(bridge.mkdirp({ filePath: "memory/kemik" })).resolves.toBeUndefined();
|
||||||
|
|
||||||
const mkdirCall = findCallByScriptFragment('mkdir -p -- "$1"');
|
const mkdirCall = findCallByScriptFragment('mkdir -p -- "$2"');
|
||||||
expect(mkdirCall).toBeDefined();
|
expect(mkdirCall).toBeDefined();
|
||||||
const mkdirPath = mkdirCall ? getDockerPathArg(mkdirCall[0]) : "";
|
const mkdirParent = mkdirCall ? getDockerArg(mkdirCall[0], 1) : "";
|
||||||
expect(mkdirPath).toBe("/workspace/memory/kemik");
|
const mkdirBase = mkdirCall ? getDockerArg(mkdirCall[0], 2) : "";
|
||||||
|
expect(mkdirParent).toBe("/workspace/memory");
|
||||||
|
expect(mkdirBase).toBe("kemik");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +252,24 @@ describe("sandbox fs bridge shell compatibility", () => {
|
|||||||
expect(scripts.some((script) => script.includes('mv -f -- "$1" "$2"'))).toBe(true);
|
expect(scripts.some((script) => script.includes('mv -f -- "$1" "$2"'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("anchors mkdirp operations on canonical parent + basename", async () => {
|
||||||
|
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
|
||||||
|
|
||||||
|
await bridge.mkdirp({ filePath: "nested/leaf" });
|
||||||
|
|
||||||
|
const mkdirCall = findCallByScriptFragment('mkdir -p -- "$2"');
|
||||||
|
expect(mkdirCall).toBeDefined();
|
||||||
|
const args = mkdirCall?.[0] ?? [];
|
||||||
|
expect(getDockerArg(args, 1)).toBe("/workspace/nested");
|
||||||
|
expect(getDockerArg(args, 2)).toBe("leaf");
|
||||||
|
expect(args).not.toContain("/workspace/nested/leaf");
|
||||||
|
|
||||||
|
const canonicalCalls = findCallsByScriptFragment('readlink -f -- "$cursor"');
|
||||||
|
expect(
|
||||||
|
canonicalCalls.some(([callArgs]) => getDockerArg(callArgs, 1) === "/workspace/nested"),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("anchors remove operations on canonical parent + basename", async () => {
|
it("anchors remove operations on canonical parent + basename", async () => {
|
||||||
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
|
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
|
||||||
|
|
||||||
@ -329,7 +349,8 @@ describe("sandbox fs bridge shell compatibility", () => {
|
|||||||
await expect(bridge.mkdirp({ filePath: "memory/kemik" })).rejects.toThrow(
|
await expect(bridge.mkdirp({ filePath: "memory/kemik" })).rejects.toThrow(
|
||||||
/cannot create directories/i,
|
/cannot create directories/i,
|
||||||
);
|
);
|
||||||
expect(mockedExecDockerRaw).not.toHaveBeenCalled();
|
const scripts = getScriptsFromCalls();
|
||||||
|
expect(scripts.some((script) => script.includes('mkdir -p -- "$2"'))).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -155,6 +155,7 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
|||||||
async mkdirp(params: { filePath: string; cwd?: string; signal?: AbortSignal }): Promise<void> {
|
async mkdirp(params: { filePath: string; cwd?: string; signal?: AbortSignal }): Promise<void> {
|
||||||
const target = this.resolveResolvedPath(params);
|
const target = this.resolveResolvedPath(params);
|
||||||
this.ensureWriteAccess(target, "create directories");
|
this.ensureWriteAccess(target, "create directories");
|
||||||
|
const anchoredTarget = await this.resolveAnchoredSandboxEntry(target);
|
||||||
await this.runCheckedCommand({
|
await this.runCheckedCommand({
|
||||||
checks: [
|
checks: [
|
||||||
{
|
{
|
||||||
@ -166,8 +167,8 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
script: 'set -eu; mkdir -p -- "$1"',
|
script: 'set -eu\ncd -- "$1"\nmkdir -p -- "$2"',
|
||||||
args: [target.containerPath],
|
args: [anchoredTarget.canonicalParentPath, anchoredTarget.basename],
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user