Skip to content

Commit ad804b0

Browse files
fix(feishu): propagate mediaLocalRoots for local file sends (#27884) (#27928) thanks @joelnishanth
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: joelnishanth <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent bf9585d commit ad804b0

File tree

4 files changed

+38
-4
lines changed

4 files changed

+38
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
1414

1515
### Fixes
1616

17+
- 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.
1718
- 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.
1819
- 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.
1920
- 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.

extensions/feishu/src/media.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,32 @@ describe("sendMediaFeishu msg_type routing", () => {
190190
expect(messageCreateMock).not.toHaveBeenCalled();
191191
});
192192

193+
it("passes mediaLocalRoots as localRoots to loadWebMedia for local paths (#27884)", async () => {
194+
loadWebMediaMock.mockResolvedValue({
195+
buffer: Buffer.from("local-file"),
196+
fileName: "doc.pdf",
197+
kind: "document",
198+
contentType: "application/pdf",
199+
});
200+
201+
const roots = ["/allowed/workspace", "/tmp/openclaw"];
202+
await sendMediaFeishu({
203+
cfg: {} as any,
204+
to: "user:ou_target",
205+
mediaUrl: "/allowed/workspace/file.pdf",
206+
mediaLocalRoots: roots,
207+
});
208+
209+
expect(loadWebMediaMock).toHaveBeenCalledWith(
210+
"/allowed/workspace/file.pdf",
211+
expect.objectContaining({
212+
maxBytes: expect.any(Number),
213+
optimizeImages: false,
214+
localRoots: roots,
215+
}),
216+
);
217+
});
218+
193219
it("fails closed when media URL fetch is blocked", async () => {
194220
loadWebMediaMock.mockRejectedValueOnce(
195221
new Error("Blocked: resolves to private/internal IP address"),

extensions/feishu/src/media.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,9 @@ export function detectFileType(
376376
}
377377

378378
/**
379-
* Upload and send media (image or file) from URL, local path, or buffer
379+
* Upload and send media (image or file) from URL, local path, or buffer.
380+
* When mediaUrl is a local path, mediaLocalRoots (from core outbound context)
381+
* must be passed so loadWebMedia allows the path (post CVE-2026-26321).
380382
*/
381383
export async function sendMediaFeishu(params: {
382384
cfg: ClawdbotConfig;
@@ -386,8 +388,11 @@ export async function sendMediaFeishu(params: {
386388
fileName?: string;
387389
replyToMessageId?: string;
388390
accountId?: string;
391+
/** Allowed roots for local path reads; required for local filePath to work. */
392+
mediaLocalRoots?: readonly string[];
389393
}): Promise<SendMediaResult> {
390-
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
394+
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId, mediaLocalRoots } =
395+
params;
391396
const account = resolveFeishuAccount({ cfg, accountId });
392397
if (!account.configured) {
393398
throw new Error(`Feishu account "${account.accountId}" not configured`);
@@ -404,6 +409,7 @@ export async function sendMediaFeishu(params: {
404409
const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
405410
maxBytes: mediaMaxBytes,
406411
optimizeImages: false,
412+
localRoots: mediaLocalRoots?.length ? mediaLocalRoots : undefined,
407413
});
408414
buffer = loaded.buffer;
409415
name = fileName ?? loaded.fileName ?? "file";

extensions/feishu/src/outbound.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ export const feishuOutbound: ChannelOutboundAdapter = {
1212
const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
1313
return { channel: "feishu", ...result };
1414
},
15-
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
15+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
1616
// Send text first if provided
1717
if (text?.trim()) {
1818
await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
1919
}
2020

21-
// Upload and send media if URL provided
21+
// Upload and send media if URL or local path provided
2222
if (mediaUrl) {
2323
try {
2424
const result = await sendMediaFeishu({
2525
cfg,
2626
to,
2727
mediaUrl,
2828
accountId: accountId ?? undefined,
29+
mediaLocalRoots,
2930
});
3031
return { channel: "feishu", ...result };
3132
} catch (err) {

0 commit comments

Comments
 (0)