fix: harden Feishu media URL fetching (#16285) (thanks @mbelinky)

Security fix for Feishu extension media fetching.
This commit is contained in:
Peter Steinberger
2026-02-14 16:42:35 +01:00
committed by GitHub
parent d82c5ea9d1
commit 5b4121d601
5 changed files with 190 additions and 50 deletions

View File

@@ -5,6 +5,7 @@ import { Readable } from "stream";
import { listEnabledFeishuAccounts } from "./accounts.js";
import { createFeishuClient } from "./client.js";
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
import { getFeishuRuntime } from "./runtime.js";
import { resolveToolsConfig } from "./tools-config.js";
// ============ Helpers ============
@@ -175,12 +176,9 @@ async function uploadImageToDocx(
return fileToken;
}
async function downloadImage(url: string): Promise<Buffer> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
async function downloadImage(url: string, maxBytes: number): Promise<Buffer> {
const fetched = await getFeishuRuntime().channel.media.fetchRemoteMedia({ url, maxBytes });
return fetched.buffer;
}
/* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
@@ -189,6 +187,7 @@ async function processImages(
docToken: string,
markdown: string,
insertedBlocks: any[],
maxBytes: number,
): Promise<number> {
/* eslint-enable @typescript-eslint/no-explicit-any */
const imageUrls = extractImageUrls(markdown);
@@ -204,7 +203,7 @@ async function processImages(
const blockId = imageBlocks[i].block_id;
try {
const buffer = await downloadImage(url);
const buffer = await downloadImage(url, maxBytes);
const urlPath = new URL(url).pathname;
const fileName = urlPath.split("/").pop() || `image_${i}.png`;
const fileToken = await uploadImageToDocx(client, blockId, buffer, fileName);
@@ -284,7 +283,7 @@ async function createDoc(client: Lark.Client, title: string, folderToken?: strin
};
}
async function writeDoc(client: Lark.Client, docToken: string, markdown: string) {
async function writeDoc(client: Lark.Client, docToken: string, markdown: string, maxBytes: number) {
const deleted = await clearDocumentContent(client, docToken);
const { blocks, firstLevelBlockIds } = await convertMarkdown(client, markdown);
@@ -294,7 +293,7 @@ async function writeDoc(client: Lark.Client, docToken: string, markdown: string)
const sortedBlocks = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
const { children: inserted, skipped } = await insertBlocks(client, docToken, sortedBlocks);
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
return {
success: true,
@@ -307,7 +306,12 @@ async function writeDoc(client: Lark.Client, docToken: string, markdown: string)
};
}
async function appendDoc(client: Lark.Client, docToken: string, markdown: string) {
async function appendDoc(
client: Lark.Client,
docToken: string,
markdown: string,
maxBytes: number,
) {
const { blocks, firstLevelBlockIds } = await convertMarkdown(client, markdown);
if (blocks.length === 0) {
throw new Error("Content is empty");
@@ -315,7 +319,7 @@ async function appendDoc(client: Lark.Client, docToken: string, markdown: string
const sortedBlocks = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
const { children: inserted, skipped } = await insertBlocks(client, docToken, sortedBlocks);
const imagesProcessed = await processImages(client, docToken, markdown, inserted);
const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
return {
success: true,
@@ -453,6 +457,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
// Use first account's config for tools configuration
const firstAccount = accounts[0];
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
const mediaMaxBytes = (firstAccount.config?.mediaMaxMb ?? 30) * 1024 * 1024;
// Helper to get client for the default account
const getClient = () => createFeishuClient(firstAccount);
@@ -475,9 +480,9 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
case "read":
return json(await readDoc(client, p.doc_token));
case "write":
return json(await writeDoc(client, p.doc_token, p.content));
return json(await writeDoc(client, p.doc_token, p.content, mediaMaxBytes));
case "append":
return json(await appendDoc(client, p.doc_token, p.content));
return json(await appendDoc(client, p.doc_token, p.content, mediaMaxBytes));
case "create":
return json(await createDoc(client, p.title, p.folder_token));
case "list_blocks":