mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:58:25 +00:00
fix: execute sandboxed file ops inside containers (#4026)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 795ec6aa2f
Co-authored-by: davidbors-snyk <240482518+davidbors-snyk@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
@@ -798,7 +798,10 @@ export async function runEmbeddedAttempt(
|
||||
historyMessages: activeSession.messages,
|
||||
maxBytes: MAX_IMAGE_BYTES,
|
||||
// Enforce sandbox path restrictions when sandbox is enabled
|
||||
sandboxRoot: sandbox?.enabled ? sandbox.workspaceDir : undefined,
|
||||
sandbox:
|
||||
sandbox?.enabled && sandbox?.fsBridge
|
||||
? { root: sandbox.workspaceDir, bridge: sandbox.fsBridge }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Inject history images into their original message positions.
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { extractTextFromMessage } from "../../../tui/tui-formatters.js";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
import { loadWebMedia } from "../../../web/media.js";
|
||||
import { assertSandboxPath } from "../../sandbox-paths.js";
|
||||
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
||||
import { sanitizeImageBlocks } from "../../tool-images.js";
|
||||
import { log } from "../logger.js";
|
||||
|
||||
@@ -177,8 +173,7 @@ export async function loadImageFromRef(
|
||||
workspaceDir: string,
|
||||
options?: {
|
||||
maxBytes?: number;
|
||||
/** If set, enforce that file paths are within this sandbox root */
|
||||
sandboxRoot?: string;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
},
|
||||
): Promise<ImageContent | null> {
|
||||
try {
|
||||
@@ -190,46 +185,34 @@ export async function loadImageFromRef(
|
||||
return null;
|
||||
}
|
||||
|
||||
// For file paths, resolve relative to the appropriate root:
|
||||
// - When sandbox is enabled, resolve relative to sandboxRoot for security
|
||||
// - Otherwise, resolve relative to workspaceDir
|
||||
// Note: ref.resolved may already be absolute (e.g., after ~ expansion in detectImageReferences),
|
||||
// in which case we skip relative resolution.
|
||||
if (ref.type === "path" && !path.isAbsolute(targetPath)) {
|
||||
const resolveRoot = options?.sandboxRoot ?? workspaceDir;
|
||||
targetPath = path.resolve(resolveRoot, targetPath);
|
||||
}
|
||||
|
||||
// Enforce sandbox restrictions if sandboxRoot is set
|
||||
if (ref.type === "path" && options?.sandboxRoot) {
|
||||
try {
|
||||
const validated = await assertSandboxPath({
|
||||
filePath: targetPath,
|
||||
cwd: options.sandboxRoot,
|
||||
root: options.sandboxRoot,
|
||||
});
|
||||
targetPath = validated.resolved;
|
||||
} catch (err) {
|
||||
// Log the actual error for debugging (sandbox violation or other path error)
|
||||
log.debug(
|
||||
`Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check file exists for local paths
|
||||
// Resolve paths relative to sandbox or workspace as needed
|
||||
if (ref.type === "path") {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
} catch {
|
||||
log.debug(`Native image: file not found: ${targetPath}`);
|
||||
return null;
|
||||
if (options?.sandbox) {
|
||||
try {
|
||||
const resolved = options.sandbox.bridge.resolvePath({
|
||||
filePath: targetPath,
|
||||
cwd: options.sandbox.root,
|
||||
});
|
||||
targetPath = resolved.hostPath;
|
||||
} catch (err) {
|
||||
log.debug(
|
||||
`Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else if (!path.isAbsolute(targetPath)) {
|
||||
targetPath = path.resolve(workspaceDir, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
// loadWebMedia handles local file paths (including file:// URLs)
|
||||
const media = await loadWebMedia(targetPath, options?.maxBytes);
|
||||
const media = options?.sandbox
|
||||
? await loadWebMedia(targetPath, {
|
||||
maxBytes: options.maxBytes,
|
||||
readFile: (filePath) =>
|
||||
options.sandbox!.bridge.readFile({ filePath, cwd: options.sandbox!.root }),
|
||||
})
|
||||
: await loadWebMedia(targetPath, options?.maxBytes);
|
||||
|
||||
if (media.kind !== "image") {
|
||||
log.debug(`Native image: not an image file: ${targetPath} (got ${media.kind})`);
|
||||
@@ -344,8 +327,7 @@ export async function detectAndLoadPromptImages(params: {
|
||||
existingImages?: ImageContent[];
|
||||
historyMessages?: unknown[];
|
||||
maxBytes?: number;
|
||||
/** If set, enforce that file paths are within this sandbox root */
|
||||
sandboxRoot?: string;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
}): Promise<{
|
||||
/** Images for the current prompt (existingImages + detected in current prompt) */
|
||||
images: ImageContent[];
|
||||
@@ -406,7 +388,7 @@ export async function detectAndLoadPromptImages(params: {
|
||||
for (const ref of allRefs) {
|
||||
const image = await loadImageFromRef(ref, params.workspaceDir, {
|
||||
maxBytes: params.maxBytes,
|
||||
sandboxRoot: params.sandboxRoot,
|
||||
sandbox: params.sandbox,
|
||||
});
|
||||
if (image) {
|
||||
if (ref.messageIndex !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user