mirror of https://github.com/openclaw/openclaw.git
fix: enforce workspaceOnly for native prompt image autoload
This commit is contained in:
parent
c3680c2277
commit
370d115549
|
|
@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- Sessions/Tool-result guard: avoid generating synthetic `toolResult` entries for assistant turns that ended with `stopReason: "aborted"` or `"error"`, preventing orphaned tool-use IDs from triggering downstream API validation errors. (#25429) Thanks @mikaeldiakhate-cell.
|
||||
- Usage accounting: parse Moonshot/Kimi `cached_tokens` fields (including `prompt_tokens_details.cached_tokens`) into normalized cache-read usage metrics. (#25436) Thanks @Elarwei001.
|
||||
- Doctor/Sandbox: when sandbox mode is enabled but Docker is unavailable, surface a clear actionable warning (including failure impact and remediation) instead of a mild “skip checks” note. (#25438) Thanks @mcaxtr.
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ For threat model + hardening guidance (including `openclaw security audit --deep
|
|||
### Tool filesystem hardening
|
||||
|
||||
- `tools.exec.applyPatch.workspaceOnly: true` (recommended): keeps `apply_patch` writes/deletes within the configured workspace directory.
|
||||
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths to the workspace directory.
|
||||
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths and native prompt image auto-load paths to the workspace directory.
|
||||
- Avoid setting `tools.exec.applyPatch.workspaceOnly: false` unless you fully trust who can trigger tool execution.
|
||||
|
||||
### Web Interface Safety
|
||||
|
|
|
|||
|
|
@ -833,7 +833,7 @@ We may add a single `readOnlyMode` flag later to simplify this configuration.
|
|||
Additional hardening options:
|
||||
|
||||
- `tools.exec.applyPatch.workspaceOnly: true` (default): ensures `apply_patch` cannot write/delete outside the workspace directory even when sandboxing is off. Set to `false` only if you intentionally want `apply_patch` to touch files outside the workspace.
|
||||
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths to the workspace directory (useful if you allow absolute paths today and want a single guardrail).
|
||||
- `tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths and native prompt image auto-load paths to the workspace directory (useful if you allow absolute paths today and want a single guardrail).
|
||||
|
||||
### 5) Secure baseline (copy/paste)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { resolveUserPath } from "../../../utils.js";
|
|||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
import { resolveOpenClawAgentDir } from "../../agent-paths.js";
|
||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||
import { resolveAgentConfig, resolveSessionAgentIds } from "../../agent-scope.js";
|
||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||
import { createCacheTrace } from "../../cache-trace.js";
|
||||
|
|
@ -363,6 +363,9 @@ export async function runEmbeddedAttempt(
|
|||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const effectiveFsWorkspaceOnly =
|
||||
(resolveAgentConfig(params.config ?? {}, sessionAgentId)?.tools?.fs?.workspaceOnly ??
|
||||
params.config?.tools?.fs?.workspaceOnly) === true;
|
||||
// Check if the model supports native image input
|
||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||
const toolsRaw = params.disableTools
|
||||
|
|
@ -1087,6 +1090,7 @@ export async function runEmbeddedAttempt(
|
|||
historyMessages: activeSession.messages,
|
||||
maxBytes: MAX_IMAGE_BYTES,
|
||||
maxDimensionPx: resolveImageSanitizationLimits(params.config).maxDimensionPx,
|
||||
workspaceOnly: effectiveFsWorkspaceOnly,
|
||||
// Enforce sandbox path restrictions when sandbox is enabled
|
||||
sandbox:
|
||||
sandbox?.enabled && sandbox?.fsBridge
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import os from "node:os";
|
|||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createHostSandboxFsBridge } from "../../test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { createUnsafeMountedSandbox } from "../../test-helpers/unsafe-mounted-sandbox.js";
|
||||
import {
|
||||
detectAndLoadPromptImages,
|
||||
detectImageReferences,
|
||||
|
|
@ -275,4 +276,76 @@ describe("detectAndLoadPromptImages", () => {
|
|||
expect(result.images).toHaveLength(0);
|
||||
expect(result.historyImagesByIndex.size).toBe(0);
|
||||
});
|
||||
|
||||
it("blocks prompt image refs outside workspace when sandbox workspaceOnly is enabled", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-native-image-sandbox-"));
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
const agentRoot = path.join(stateDir, "agent");
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.mkdir(agentRoot, { recursive: true });
|
||||
const pngB64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
await fs.writeFile(path.join(agentRoot, "secret.png"), Buffer.from(pngB64, "base64"));
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot });
|
||||
const bridge = sandbox.fsBridge;
|
||||
if (!bridge) {
|
||||
throw new Error("sandbox fs bridge missing");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await detectAndLoadPromptImages({
|
||||
prompt: "Inspect /agent/secret.png",
|
||||
workspaceDir: sandboxRoot,
|
||||
model: { input: ["text", "image"] },
|
||||
workspaceOnly: true,
|
||||
sandbox: { root: sandbox.workspaceDir, bridge },
|
||||
});
|
||||
|
||||
expect(result.detectedRefs).toHaveLength(1);
|
||||
expect(result.loadedCount).toBe(0);
|
||||
expect(result.skippedCount).toBe(1);
|
||||
expect(result.images).toHaveLength(0);
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("blocks history image refs outside workspace when sandbox workspaceOnly is enabled", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-native-image-sandbox-"));
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
const agentRoot = path.join(stateDir, "agent");
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.mkdir(agentRoot, { recursive: true });
|
||||
const pngB64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
await fs.writeFile(path.join(agentRoot, "secret.png"), Buffer.from(pngB64, "base64"));
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot });
|
||||
const bridge = sandbox.fsBridge;
|
||||
if (!bridge) {
|
||||
throw new Error("sandbox fs bridge missing");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await detectAndLoadPromptImages({
|
||||
prompt: "No inline image in this turn.",
|
||||
workspaceDir: sandboxRoot,
|
||||
model: { input: ["text", "image"] },
|
||||
workspaceOnly: true,
|
||||
historyMessages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Previous image /agent/secret.png" }],
|
||||
},
|
||||
],
|
||||
sandbox: { root: sandbox.workspaceDir, bridge },
|
||||
});
|
||||
|
||||
expect(result.detectedRefs).toHaveLength(1);
|
||||
expect(result.loadedCount).toBe(0);
|
||||
expect(result.skippedCount).toBe(1);
|
||||
expect(result.historyImagesByIndex.size).toBe(0);
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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 { assertSandboxPath } from "../../sandbox-paths.js";
|
||||
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
||||
import { sanitizeImageBlocks } from "../../tool-images.js";
|
||||
import { log } from "../logger.js";
|
||||
|
|
@ -181,6 +182,7 @@ export async function loadImageFromRef(
|
|||
workspaceDir: string,
|
||||
options?: {
|
||||
maxBytes?: number;
|
||||
workspaceOnly?: boolean;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
},
|
||||
): Promise<ImageContent | null> {
|
||||
|
|
@ -211,6 +213,14 @@ export async function loadImageFromRef(
|
|||
} else if (!path.isAbsolute(targetPath)) {
|
||||
targetPath = path.resolve(workspaceDir, targetPath);
|
||||
}
|
||||
if (options?.workspaceOnly) {
|
||||
const root = options?.sandbox?.root ?? workspaceDir;
|
||||
await assertSandboxPath({
|
||||
filePath: targetPath,
|
||||
cwd: root,
|
||||
root,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// loadWebMedia handles local file paths (including file:// URLs)
|
||||
|
|
@ -361,6 +371,7 @@ export async function detectAndLoadPromptImages(params: {
|
|||
historyMessages?: unknown[];
|
||||
maxBytes?: number;
|
||||
maxDimensionPx?: number;
|
||||
workspaceOnly?: boolean;
|
||||
sandbox?: { root: string; bridge: SandboxFsBridge };
|
||||
}): Promise<{
|
||||
/** Images for the current prompt (existingImages + detected in current prompt) */
|
||||
|
|
@ -422,6 +433,7 @@ export async function detectAndLoadPromptImages(params: {
|
|||
for (const ref of allRefs) {
|
||||
const image = await loadImageFromRef(ref, params.workspaceDir, {
|
||||
maxBytes: params.maxBytes,
|
||||
workspaceOnly: params.workspaceOnly,
|
||||
sandbox: params.sandbox,
|
||||
});
|
||||
if (image) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue