mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 08:07:27 +00:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user