mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
fix(sandbox): block @-prefixed workspace path bypass
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security/Workspace FS: normalize `@`-prefixed paths before workspace-boundary checks (including workspace-only read/write/edit and sandbox mount path guards), preventing absolute-path escape attempts from bypassing guard validation. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Native images: enforce `tools.fs.workspaceOnly` for native prompt image auto-load (including history refs), preventing out-of-workspace sandbox mounts from being implicitly ingested as vision input. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Exec approvals: bind `system.run` command display/approval text to full argv when shell-wrapper inline payloads carry positional argv values, and reject payload-only `rawCommand` mismatches for those wrapper-carrier forms, preventing hidden command execution under misleading approval text. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg.
|
||||
|
||||
@@ -571,7 +571,7 @@ function mapContainerPathToWorkspaceRoot(params: {
|
||||
return params.filePath;
|
||||
}
|
||||
|
||||
let candidate = params.filePath;
|
||||
let candidate = params.filePath.startsWith("@") ? params.filePath.slice(1) : params.filePath;
|
||||
if (/^file:\/\//i.test(candidate)) {
|
||||
try {
|
||||
candidate = fileURLToPath(candidate);
|
||||
|
||||
@@ -61,6 +61,36 @@ describe("wrapToolWorkspaceRootGuardWithOptions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps @-prefixed container workspace paths to host workspace root", async () => {
|
||||
const { tool } = createToolHarness();
|
||||
const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
|
||||
containerWorkdir: "/workspace",
|
||||
});
|
||||
|
||||
await wrapped.execute("tc-at-container", { path: "@/workspace/docs/readme.md" });
|
||||
|
||||
expect(mocks.assertSandboxPath).toHaveBeenCalledWith({
|
||||
filePath: path.resolve(root, "docs", "readme.md"),
|
||||
cwd: root,
|
||||
root,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes @-prefixed absolute paths before guard checks", async () => {
|
||||
const { tool } = createToolHarness();
|
||||
const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
|
||||
containerWorkdir: "/workspace",
|
||||
});
|
||||
|
||||
await wrapped.execute("tc-at-absolute", { path: "@/etc/passwd" });
|
||||
|
||||
expect(mocks.assertSandboxPath).toHaveBeenCalledWith({
|
||||
filePath: "/etc/passwd",
|
||||
cwd: root,
|
||||
root,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not remap absolute paths outside the configured container workdir", async () => {
|
||||
const { tool } = createToolHarness();
|
||||
const wrapped = wrapToolWorkspaceRootGuardWithOptions(tool, root, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { expectReadWriteEditTools, getTextContent } from "./test-helpers/pi-tools-fs-helpers.js";
|
||||
@@ -137,6 +138,19 @@ describe("workspace path resolution", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects @-prefixed absolute paths outside workspace when workspaceOnly is enabled", async () => {
|
||||
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
||||
const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
|
||||
const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
|
||||
const { readTool } = expectReadWriteEditTools(tools);
|
||||
|
||||
const outsideAbsolute = path.resolve(path.parse(workspaceDir).root, "outside-openclaw.txt");
|
||||
await expect(
|
||||
readTool.execute("ws-read-at-prefix", { path: `@${outsideAbsolute}` }),
|
||||
).rejects.toThrow(/Path escapes sandbox root/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sandboxed workspace paths", () => {
|
||||
|
||||
@@ -13,8 +13,12 @@ function normalizeUnicodeSpaces(str: string): string {
|
||||
return str.replace(UNICODE_SPACES, " ");
|
||||
}
|
||||
|
||||
function normalizeAtPrefix(filePath: string): string {
|
||||
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
||||
}
|
||||
|
||||
function expandPath(filePath: string): string {
|
||||
const normalized = normalizeUnicodeSpaces(filePath);
|
||||
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
||||
if (normalized === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
|
||||
@@ -227,7 +227,13 @@ function isPathInsidePosix(root: string, target: string): boolean {
|
||||
|
||||
function isPathInsideHost(root: string, target: string): boolean {
|
||||
const canonicalRoot = resolveSandboxHostPathViaExistingAncestor(path.resolve(root));
|
||||
const canonicalTarget = resolveSandboxHostPathViaExistingAncestor(path.resolve(target));
|
||||
const resolvedTarget = path.resolve(target);
|
||||
// Preserve the final path segment so pre-existing symlink leaves are validated
|
||||
// by the dedicated symlink guard later in the bridge flow.
|
||||
const canonicalTargetParent = resolveSandboxHostPathViaExistingAncestor(
|
||||
path.dirname(resolvedTarget),
|
||||
);
|
||||
const canonicalTarget = path.resolve(canonicalTargetParent, path.basename(resolvedTarget));
|
||||
const rel = path.relative(canonicalRoot, canonicalTarget);
|
||||
if (!rel) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user