diff --git a/src/infra/outbound/message-action-params.ts b/src/infra/outbound/message-action-params.ts index b24146cb9..a73912edc 100644 --- a/src/infra/outbound/message-action-params.ts +++ b/src/infra/outbound/message-action-params.ts @@ -279,9 +279,10 @@ async function hydrateAttachmentPayload(params: { export async function normalizeSandboxMediaParams(params: { args: Record; - sandboxRoot?: string; + mediaPolicy: AttachmentMediaPolicy; }): Promise { - const sandboxRoot = params.sandboxRoot?.trim(); + const sandboxRoot = + params.mediaPolicy.mode === "sandbox" ? params.mediaPolicy.sandboxRoot.trim() : undefined; const mediaKeys: Array<"media" | "path" | "filePath"> = ["media", "path", "filePath"]; for (const key of mediaKeys) { const raw = readStringParam(params.args, key, { trim: false }); @@ -362,7 +363,7 @@ async function hydrateAttachmentActionPayload(params: { }); } -export async function hydrateSetGroupIconParams(params: { +export async function hydrateAttachmentParamsForAction(params: { cfg: OpenClawConfig; channel: ChannelId; accountId?: string | null; @@ -371,25 +372,18 @@ export async function hydrateSetGroupIconParams(params: { dryRun?: boolean; mediaPolicy: AttachmentMediaPolicy; }): Promise { - if (params.action !== "setGroupIcon") { + if (params.action !== "sendAttachment" && params.action !== "setGroupIcon") { return; } - await hydrateAttachmentActionPayload(params); -} - -export async function hydrateSendAttachmentParams(params: { - cfg: OpenClawConfig; - channel: ChannelId; - accountId?: string | null; - args: Record; - action: ChannelMessageActionName; - dryRun?: boolean; - mediaPolicy: AttachmentMediaPolicy; -}): Promise { - if (params.action !== "sendAttachment") { - return; - } - await hydrateAttachmentActionPayload({ ...params, allowMessageCaptionFallback: true }); + await hydrateAttachmentActionPayload({ + cfg: params.cfg, + channel: params.channel, + accountId: params.accountId, + args: params.args, + dryRun: params.dryRun, + mediaPolicy: params.mediaPolicy, + allowMessageCaptionFallback: params.action === "sendAttachment", + }); } export function parseButtonsParam(params: Record): void { diff --git a/src/infra/outbound/message-action-runner.test.ts b/src/infra/outbound/message-action-runner.test.ts index 127a38380..ed1fbf47e 100644 --- a/src/infra/outbound/message-action-runner.test.ts +++ b/src/infra/outbound/message-action-runner.test.ts @@ -490,6 +490,40 @@ describe("runMessageAction sendAttachment hydration", () => { vi.mocked(loadWebMedia).mockImplementation(actual.loadWebMedia); } + async function expectRejectsLocalAbsolutePathWithoutSandbox(params: { + action: "sendAttachment" | "setGroupIcon"; + target: string; + message?: string; + tempPrefix: string; + }) { + await restoreRealMediaLoader(); + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), params.tempPrefix)); + try { + const outsidePath = path.join(tempDir, "secret.txt"); + await fs.writeFile(outsidePath, "secret", "utf8"); + + const actionParams: Record = { + channel: "bluebubbles", + target: params.target, + media: outsidePath, + }; + if (params.message) { + actionParams.message = params.message; + } + + await expect( + runMessageAction({ + cfg, + action: params.action, + params: actionParams, + }), + ).rejects.toThrow(/allowed directory|path-not-allowed/i); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + } + it("hydrates buffer and filename from media for sendAttachment", async () => { const result = await runMessageAction({ cfg, @@ -548,52 +582,20 @@ describe("runMessageAction sendAttachment hydration", () => { }); it("rejects local absolute path for sendAttachment when sandboxRoot is missing", async () => { - await restoreRealMediaLoader(); - - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-attachment-")); - try { - const outsidePath = path.join(tempDir, "secret.txt"); - await fs.writeFile(outsidePath, "secret", "utf8"); - - await expect( - runMessageAction({ - cfg, - action: "sendAttachment", - params: { - channel: "bluebubbles", - target: "+15551234567", - media: outsidePath, - message: "caption", - }, - }), - ).rejects.toThrow(/allowed directory|path-not-allowed/i); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + await expectRejectsLocalAbsolutePathWithoutSandbox({ + action: "sendAttachment", + target: "+15551234567", + message: "caption", + tempPrefix: "msg-attachment-", + }); }); it("rejects local absolute path for setGroupIcon when sandboxRoot is missing", async () => { - await restoreRealMediaLoader(); - - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-group-icon-")); - try { - const outsidePath = path.join(tempDir, "secret.txt"); - await fs.writeFile(outsidePath, "secret", "utf8"); - - await expect( - runMessageAction({ - cfg, - action: "setGroupIcon", - params: { - channel: "bluebubbles", - target: "group:123", - media: outsidePath, - }, - }), - ).rejects.toThrow(/allowed directory|path-not-allowed/i); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + await expectRejectsLocalAbsolutePathWithoutSandbox({ + action: "setGroupIcon", + target: "group:123", + tempPrefix: "msg-group-icon-", + }); }); }); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 68a75f0c0..57032e27d 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -28,8 +28,7 @@ import { import { applyTargetToParams } from "./channel-target.js"; import type { OutboundSendDeps } from "./deliver.js"; import { - hydrateSendAttachmentParams, - hydrateSetGroupIconParams, + hydrateAttachmentParamsForAction, normalizeSandboxMediaList, normalizeSandboxMediaParams, parseButtonsParam, @@ -767,20 +766,10 @@ export async function runMessageAction( await normalizeSandboxMediaParams({ args: params, - sandboxRoot: mediaPolicy.mode === "sandbox" ? mediaPolicy.sandboxRoot : undefined, - }); - - await hydrateSendAttachmentParams({ - cfg, - channel, - accountId, - args: params, - action, - dryRun, mediaPolicy, }); - await hydrateSetGroupIconParams({ + await hydrateAttachmentParamsForAction({ cfg, channel, accountId,