fix(feishu): propagate mediaLocalRoots for local file sends (#27884) (openclaw#27928) thanks @joelnishanth

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: joelnishanth <140015627+joelnishanth@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
OfflynAI
2026-02-27 15:43:57 -08:00
committed by GitHub
parent bf9585d056
commit ad804b0356
4 changed files with 38 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
### Fixes ### 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. - 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. - 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. - 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.

View File

@@ -190,6 +190,32 @@ describe("sendMediaFeishu msg_type routing", () => {
expect(messageCreateMock).not.toHaveBeenCalled(); 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 () => { it("fails closed when media URL fetch is blocked", async () => {
loadWebMediaMock.mockRejectedValueOnce( loadWebMediaMock.mockRejectedValueOnce(
new Error("Blocked: resolves to private/internal IP address"), new Error("Blocked: resolves to private/internal IP address"),

View File

@@ -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: { export async function sendMediaFeishu(params: {
cfg: ClawdbotConfig; cfg: ClawdbotConfig;
@@ -386,8 +388,11 @@ export async function sendMediaFeishu(params: {
fileName?: string; fileName?: string;
replyToMessageId?: string; replyToMessageId?: string;
accountId?: string; accountId?: string;
/** Allowed roots for local path reads; required for local filePath to work. */
mediaLocalRoots?: readonly string[];
}): Promise<SendMediaResult> { }): Promise<SendMediaResult> {
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params; const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId, mediaLocalRoots } =
params;
const account = resolveFeishuAccount({ cfg, accountId }); const account = resolveFeishuAccount({ cfg, accountId });
if (!account.configured) { if (!account.configured) {
throw new Error(`Feishu account "${account.accountId}" not 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, { const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
maxBytes: mediaMaxBytes, maxBytes: mediaMaxBytes,
optimizeImages: false, optimizeImages: false,
localRoots: mediaLocalRoots?.length ? mediaLocalRoots : undefined,
}); });
buffer = loaded.buffer; buffer = loaded.buffer;
name = fileName ?? loaded.fileName ?? "file"; name = fileName ?? loaded.fileName ?? "file";

View File

@@ -12,13 +12,13 @@ export const feishuOutbound: ChannelOutboundAdapter = {
const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
return { channel: "feishu", ...result }; return { channel: "feishu", ...result };
}, },
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
// Send text first if provided // Send text first if provided
if (text?.trim()) { if (text?.trim()) {
await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); 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) { if (mediaUrl) {
try { try {
const result = await sendMediaFeishu({ const result = await sendMediaFeishu({
@@ -26,6 +26,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to, to,
mediaUrl, mediaUrl,
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
mediaLocalRoots,
}); });
return { channel: "feishu", ...result }; return { channel: "feishu", ...result };
} catch (err) { } catch (err) {