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

@@ -4,6 +4,7 @@ const createFeishuClientMock = vi.hoisted(() => vi.fn());
const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
const normalizeFeishuTargetMock = vi.hoisted(() => vi.fn());
const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
const loadWebMediaMock = vi.hoisted(() => vi.fn());
const fileCreateMock = vi.hoisted(() => vi.fn());
const messageCreateMock = vi.hoisted(() => vi.fn());
@@ -22,6 +23,14 @@ vi.mock("./targets.js", () => ({
resolveReceiveIdType: resolveReceiveIdTypeMock,
}));
vi.mock("./runtime.js", () => ({
getFeishuRuntime: () => ({
media: {
loadWebMedia: loadWebMediaMock,
},
}),
}));
import { sendMediaFeishu } from "./media.js";
describe("sendMediaFeishu msg_type routing", () => {
@@ -31,6 +40,7 @@ describe("sendMediaFeishu msg_type routing", () => {
resolveFeishuAccountMock.mockReturnValue({
configured: true,
accountId: "main",
config: {},
appId: "app_id",
appSecret: "app_secret",
domain: "feishu",
@@ -65,6 +75,13 @@ describe("sendMediaFeishu msg_type routing", () => {
code: 0,
data: { message_id: "reply_1" },
});
loadWebMediaMock.mockResolvedValue({
buffer: Buffer.from("remote-audio"),
fileName: "remote.opus",
kind: "audio",
contentType: "audio/ogg",
});
});
it("uses msg_type=media for mp4", async () => {
@@ -148,4 +165,23 @@ describe("sendMediaFeishu msg_type routing", () => {
expect(messageCreateMock).not.toHaveBeenCalled();
});
it("fails closed when media URL fetch is blocked", async () => {
loadWebMediaMock.mockRejectedValueOnce(
new Error("Blocked: resolves to private/internal IP address"),
);
await expect(
sendMediaFeishu({
cfg: {} as any,
to: "user:ou_target",
mediaUrl: "https://x/img",
fileName: "voice.opus",
}),
).rejects.toThrow(/private\/internal/i);
expect(fileCreateMock).not.toHaveBeenCalled();
expect(messageCreateMock).not.toHaveBeenCalled();
expect(messageReplyMock).not.toHaveBeenCalled();
});
});