mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 08:18:34 +00:00
fix: harden sandbox media reads against TOCTOU escapes
This commit is contained in:
57
src/infra/outbound/message-action-params.test.ts
Normal file
57
src/infra/outbound/message-action-params.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
hydrateAttachmentParamsForAction,
|
||||
normalizeSandboxMediaParams,
|
||||
} from "./message-action-params.js";
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const maybeIt = process.platform === "win32" ? it.skip : it;
|
||||
|
||||
describe("message action sandbox media hydration", () => {
|
||||
maybeIt("rejects symlink retarget escapes after sandbox media normalization", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-sandbox-"));
|
||||
const outsideRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-outside-"));
|
||||
try {
|
||||
const insideDir = path.join(sandboxRoot, "inside");
|
||||
await fs.mkdir(insideDir, { recursive: true });
|
||||
await fs.writeFile(path.join(insideDir, "note.txt"), "INSIDE_SECRET", "utf8");
|
||||
await fs.writeFile(path.join(outsideRoot, "note.txt"), "OUTSIDE_SECRET", "utf8");
|
||||
|
||||
const slotLink = path.join(sandboxRoot, "slot");
|
||||
await fs.symlink(insideDir, slotLink);
|
||||
|
||||
const args: Record<string, unknown> = {
|
||||
media: "slot/note.txt",
|
||||
};
|
||||
const mediaPolicy = {
|
||||
mode: "sandbox",
|
||||
sandboxRoot,
|
||||
} as const;
|
||||
|
||||
await normalizeSandboxMediaParams({
|
||||
args,
|
||||
mediaPolicy,
|
||||
});
|
||||
|
||||
await fs.rm(slotLink, { recursive: true, force: true });
|
||||
await fs.symlink(outsideRoot, slotLink);
|
||||
|
||||
await expect(
|
||||
hydrateAttachmentParamsForAction({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
args,
|
||||
action: "sendAttachment",
|
||||
mediaPolicy,
|
||||
}),
|
||||
).rejects.toThrow(/outside workspace root|outside/i);
|
||||
} finally {
|
||||
await fs.rm(sandboxRoot, { recursive: true, force: true });
|
||||
await fs.rm(outsideRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { assertMediaNotDataUrl, resolveSandboxedMediaSource } from "../../agents/sandbox-paths.js";
|
||||
@@ -9,6 +8,7 @@ import type {
|
||||
ChannelThreadingToolContext,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createRootScopedReadFile } from "../../infra/fs-safe.js";
|
||||
import { extensionForMime } from "../../media/mime.js";
|
||||
import { parseSlackTarget } from "../../slack/targets.js";
|
||||
import { parseTelegramTarget } from "../../telegram/targets.js";
|
||||
@@ -210,10 +210,13 @@ function buildAttachmentMediaLoadOptions(params: {
|
||||
localRoots?: readonly string[];
|
||||
} {
|
||||
if (params.policy.mode === "sandbox") {
|
||||
const readSandboxFile = createRootScopedReadFile({
|
||||
rootDir: params.policy.sandboxRoot.trim(),
|
||||
});
|
||||
return {
|
||||
maxBytes: params.maxBytes,
|
||||
sandboxValidated: true,
|
||||
readFile: (filePath: string) => fs.readFile(filePath),
|
||||
readFile: readSandboxFile,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user