diff --git a/CHANGELOG.md b/CHANGELOG.md index 4679c37ee..2214c3f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Feishu/Local media sends: propagate `mediaLocalRoots` through Feishu outbound media sending into `loadWebMedia` so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth. - Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3. - Telegram/Reply media context: include replied media files in inbound context when replying to media, defer reply-media downloads to debounce flush, gate reply-media fetch behind DM authorization, and preserve replied media when non-vision sticker fallback runs (including cached-sticker paths). (#28488) Thanks @obviyus. - Gateway/WS: close repeated post-handshake `unauthorized role:*` request floods per connection and sample duplicate rejection logs, preventing a single misbehaving client from degrading gateway responsiveness. (#20168) Thanks @acy103, @vibecodooor, and @vincentkoc. diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index fc600481e..529516d36 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -190,6 +190,32 @@ describe("sendMediaFeishu msg_type routing", () => { expect(messageCreateMock).not.toHaveBeenCalled(); }); + it("passes mediaLocalRoots as localRoots to loadWebMedia for local paths (#27884)", async () => { + loadWebMediaMock.mockResolvedValue({ + buffer: Buffer.from("local-file"), + fileName: "doc.pdf", + kind: "document", + contentType: "application/pdf", + }); + + const roots = ["/allowed/workspace", "/tmp/openclaw"]; + await sendMediaFeishu({ + cfg: {} as any, + to: "user:ou_target", + mediaUrl: "/allowed/workspace/file.pdf", + mediaLocalRoots: roots, + }); + + expect(loadWebMediaMock).toHaveBeenCalledWith( + "/allowed/workspace/file.pdf", + expect.objectContaining({ + maxBytes: expect.any(Number), + optimizeImages: false, + localRoots: roots, + }), + ); + }); + it("fails closed when media URL fetch is blocked", async () => { loadWebMediaMock.mockRejectedValueOnce( new Error("Blocked: resolves to private/internal IP address"), diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 73c5ff265..d6462006e 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -376,7 +376,9 @@ export function detectFileType( } /** - * Upload and send media (image or file) from URL, local path, or buffer + * Upload and send media (image or file) from URL, local path, or buffer. + * When mediaUrl is a local path, mediaLocalRoots (from core outbound context) + * must be passed so loadWebMedia allows the path (post CVE-2026-26321). */ export async function sendMediaFeishu(params: { cfg: ClawdbotConfig; @@ -386,8 +388,11 @@ export async function sendMediaFeishu(params: { fileName?: string; replyToMessageId?: string; accountId?: string; + /** Allowed roots for local path reads; required for local filePath to work. */ + mediaLocalRoots?: readonly string[]; }): Promise { - const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params; + const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId, mediaLocalRoots } = + params; const account = resolveFeishuAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); @@ -404,6 +409,7 @@ export async function sendMediaFeishu(params: { const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, { maxBytes: mediaMaxBytes, optimizeImages: false, + localRoots: mediaLocalRoots?.length ? mediaLocalRoots : undefined, }); buffer = loaded.buffer; name = fileName ?? loaded.fileName ?? "file"; diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index 50f385525..d1f43022a 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -12,13 +12,13 @@ export const feishuOutbound: ChannelOutboundAdapter = { const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); return { channel: "feishu", ...result }; }, - sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => { // Send text first if provided if (text?.trim()) { await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); } - // Upload and send media if URL provided + // Upload and send media if URL or local path provided if (mediaUrl) { try { const result = await sendMediaFeishu({ @@ -26,6 +26,7 @@ export const feishuOutbound: ChannelOutboundAdapter = { to, mediaUrl, accountId: accountId ?? undefined, + mediaLocalRoots, }); return { channel: "feishu", ...result }; } catch (err) {