mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:12:43 +00:00
fix: harden sandbox media reads against TOCTOU escapes
This commit is contained in:
@@ -4,7 +4,10 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
import { loadWebMedia } from "../../../web/media.js";
|
||||
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import { resolveSandboxedBridgeMediaPath } from "../../sandbox-media-paths.js";
|
||||
import {
|
||||
createSandboxBridgeReadFile,
|
||||
resolveSandboxedBridgeMediaPath,
|
||||
} from "../../sandbox-media-paths.js";
|
||||
import { assertSandboxPath } from "../../sandbox-paths.js";
|
||||
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
||||
import { sanitizeImageBlocks } from "../../tool-images.js";
|
||||
@@ -235,8 +238,7 @@ export async function loadImageFromRef(
|
||||
? await loadWebMedia(targetPath, {
|
||||
maxBytes: options.maxBytes,
|
||||
sandboxValidated: true,
|
||||
readFile: (filePath) =>
|
||||
options.sandbox!.bridge.readFile({ filePath, cwd: options.sandbox!.root }),
|
||||
readFile: createSandboxBridgeReadFile({ sandbox: options.sandbox }),
|
||||
})
|
||||
: await loadWebMedia(targetPath, options?.maxBytes);
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent";
|
||||
import { SafeOpenError, openFileWithinRoot, writeFileWithinRoot } from "../infra/fs-safe.js";
|
||||
import {
|
||||
SafeOpenError,
|
||||
openFileWithinRoot,
|
||||
readFileWithinRoot,
|
||||
writeFileWithinRoot,
|
||||
} from "../infra/fs-safe.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { sniffMimeFromBase64 } from "../media/sniff-mime-from-base64.js";
|
||||
import type { ImageSanitizationLimits } from "./image-sanitization.js";
|
||||
@@ -823,15 +828,11 @@ function createHostEditOperations(root: string, options?: { workspaceOnly?: bool
|
||||
return {
|
||||
readFile: async (absolutePath: string) => {
|
||||
const relative = toRelativePathInRoot(root, absolutePath);
|
||||
const opened = await openFileWithinRoot({
|
||||
const safeRead = await readFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: relative,
|
||||
});
|
||||
try {
|
||||
return await opened.handle.readFile();
|
||||
} finally {
|
||||
await opened.handle.close().catch(() => {});
|
||||
}
|
||||
return safeRead.buffer;
|
||||
},
|
||||
writeFile: async (absolutePath: string, content: string) => {
|
||||
const relative = toRelativePathInRoot(root, absolutePath);
|
||||
|
||||
22
src/agents/sandbox-media-paths.test.ts
Normal file
22
src/agents/sandbox-media-paths.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createSandboxBridgeReadFile } from "./sandbox-media-paths.js";
|
||||
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
|
||||
|
||||
describe("createSandboxBridgeReadFile", () => {
|
||||
it("delegates reads through the sandbox bridge with sandbox root cwd", async () => {
|
||||
const readFile = vi.fn(async () => Buffer.from("ok"));
|
||||
const scopedRead = createSandboxBridgeReadFile({
|
||||
sandbox: {
|
||||
root: "/tmp/sandbox-root",
|
||||
bridge: {
|
||||
readFile,
|
||||
} as unknown as SandboxFsBridge,
|
||||
},
|
||||
});
|
||||
await expect(scopedRead("media/inbound/example.png")).resolves.toEqual(Buffer.from("ok"));
|
||||
expect(readFile).toHaveBeenCalledWith({
|
||||
filePath: "media/inbound/example.png",
|
||||
cwd: "/tmp/sandbox-root",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,16 @@ export type SandboxedBridgeMediaPathConfig = {
|
||||
workspaceOnly?: boolean;
|
||||
};
|
||||
|
||||
export function createSandboxBridgeReadFile(params: {
|
||||
sandbox: Pick<SandboxedBridgeMediaPathConfig, "root" | "bridge">;
|
||||
}): (filePath: string) => Promise<Buffer> {
|
||||
return async (filePath: string) =>
|
||||
await params.sandbox.bridge.readFile({
|
||||
filePath,
|
||||
cwd: params.sandbox.root,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveSandboxedBridgeMediaPath(params: {
|
||||
sandbox: SandboxedBridgeMediaPathConfig;
|
||||
mediaPath: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { resolveConfiguredModelRef } from "../model-selection.js";
|
||||
import { ensureOpenClawModelsJson } from "../models-config.js";
|
||||
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
||||
import {
|
||||
createSandboxBridgeReadFile,
|
||||
resolveSandboxedBridgeMediaPath,
|
||||
type SandboxedBridgeMediaPathConfig,
|
||||
} from "../sandbox-media-paths.js";
|
||||
@@ -496,8 +497,7 @@ export function createImageTool(options?: {
|
||||
? await loadWebMedia(resolvedPath ?? resolvedImage, {
|
||||
maxBytes,
|
||||
sandboxValidated: true,
|
||||
readFile: (filePath) =>
|
||||
sandboxConfig.bridge.readFile({ filePath, cwd: sandboxConfig.root }),
|
||||
readFile: createSandboxBridgeReadFile({ sandbox: sandboxConfig }),
|
||||
})
|
||||
: await loadWebMedia(resolvedPath ?? resolvedImage, {
|
||||
maxBytes,
|
||||
|
||||
Reference in New Issue
Block a user