fix(security): lock sandbox tmp media paths to openclaw roots

This commit is contained in:
Peter Steinberger
2026-02-24 23:09:34 +00:00
parent bf8ca07deb
commit d3da67c7a9
13 changed files with 364 additions and 31 deletions

View File

@@ -11,6 +11,7 @@ import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/c
import { withEnvAsync } from "../../test-utils/env.js";
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js";
import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js";
const mocks = vi.hoisted(() => ({
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
@@ -202,6 +203,86 @@ describe("deliverOutboundPayloads", () => {
);
});
it("includes OpenClaw tmp root in telegram mediaLocalRoots", async () => {
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
await deliverOutboundPayloads({
cfg: telegramChunkConfig,
channel: "telegram",
to: "123",
payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
deps: { sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledWith(
"123",
"hi",
expect.objectContaining({
mediaLocalRoots: expect.arrayContaining([resolvePreferredOpenClawTmpDir()]),
}),
);
});
it("includes OpenClaw tmp root in signal mediaLocalRoots", async () => {
const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 });
await deliverOutboundPayloads({
cfg: { channels: { signal: {} } },
channel: "signal",
to: "+1555",
payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
deps: { sendSignal },
});
expect(sendSignal).toHaveBeenCalledWith(
"+1555",
"hi",
expect.objectContaining({
mediaLocalRoots: expect.arrayContaining([resolvePreferredOpenClawTmpDir()]),
}),
);
});
it("includes OpenClaw tmp root in whatsapp mediaLocalRoots", async () => {
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
await deliverOutboundPayloads({
cfg: whatsappChunkConfig,
channel: "whatsapp",
to: "+1555",
payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
deps: { sendWhatsApp },
});
expect(sendWhatsApp).toHaveBeenCalledWith(
"+1555",
"hi",
expect.objectContaining({
mediaLocalRoots: expect.arrayContaining([resolvePreferredOpenClawTmpDir()]),
}),
);
});
it("includes OpenClaw tmp root in imessage mediaLocalRoots", async () => {
const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1", chatId: "chat-1" });
await deliverOutboundPayloads({
cfg: {},
channel: "imessage",
to: "imessage:+15551234567",
payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
deps: { sendIMessage },
});
expect(sendIMessage).toHaveBeenCalledWith(
"imessage:+15551234567",
"hi",
expect.objectContaining({
mediaLocalRoots: expect.arrayContaining([resolvePreferredOpenClawTmpDir()]),
}),
);
});
it("uses signal media maxBytes from config", async () => {
const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 });
const cfg: OpenClawConfig = { channels: { signal: { mediaMaxMb: 2 } } };

View File

@@ -12,6 +12,7 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
import { loadWebMedia } from "../../web/media.js";
import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js";
import { runMessageAction } from "./message-action-runner.js";
vi.mock("../../web/media.js", async () => {
@@ -622,10 +623,12 @@ describe("runMessageAction sandboxed media validation", () => {
});
});
it("allows media paths under os.tmpdir()", async () => {
it("allows media paths under preferred OpenClaw tmp root", async () => {
const tmpRoot = resolvePreferredOpenClawTmpDir();
await fs.mkdir(tmpRoot, { recursive: true });
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-"));
try {
const tmpFile = path.join(os.tmpdir(), "test-media-image.png");
const tmpFile = path.join(tmpRoot, "test-media-image.png");
const result = await runMessageAction({
cfg: slackConfig,
action: "send",
@@ -644,6 +647,21 @@ describe("runMessageAction sandboxed media validation", () => {
throw new Error("expected send result");
}
expect(result.sendResult?.mediaUrl).toBe(tmpFile);
const hostTmpOutsideOpenClaw = path.join(os.tmpdir(), "outside-openclaw", "test-media.png");
await expect(
runMessageAction({
cfg: slackConfig,
action: "send",
params: {
channel: "slack",
target: "#C12345678",
media: hostTmpOutsideOpenClaw,
message: "",
},
sandboxRoot: sandboxDir,
dryRun: true,
}),
).rejects.toThrow(/sandbox/i);
} finally {
await fs.rm(sandboxDir, { recursive: true, force: true });
}