fix(discord): keep attachment metadata when media fetch is blocked

Preserve inbound attachment/sticker metadata in Discord message context when media download fails (for example due to SSRF blocking), so agents still see file references instead of silent drops.

Closes #28816
This commit is contained in:
SidQin-cyber
2026-02-27 23:55:11 +08:00
committed by Peter Steinberger
parent e4e5d9c98c
commit 0a67033fe3
2 changed files with 106 additions and 0 deletions

View File

@@ -139,6 +139,34 @@ describe("resolveForwardedMediaList", () => {
);
});
it("keeps forwarded attachment metadata when download fails", async () => {
const attachment = {
id: "att-fallback",
url: "https://cdn.discordapp.com/attachments/1/fallback.png",
filename: "fallback.png",
content_type: "image/png",
};
fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard"));
const result = await resolveForwardedMediaList(
asMessage({
rawData: {
message_snapshots: [{ message: { attachments: [attachment] } }],
},
}),
512,
);
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(result).toEqual([
{
path: attachment.url,
contentType: "image/png",
placeholder: "<media:image>",
},
]);
});
it("downloads forwarded stickers", async () => {
const sticker = {
id: "sticker-1",
@@ -279,6 +307,57 @@ describe("resolveMediaList", () => {
expect.objectContaining({ fetchImpl: proxyFetch }),
);
});
it("keeps attachment metadata when download fails", async () => {
const attachment = {
id: "att-main-fallback",
url: "https://cdn.discordapp.com/attachments/1/main-fallback.png",
filename: "main-fallback.png",
content_type: "image/png",
};
fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard"));
const result = await resolveMediaList(
asMessage({
attachments: [attachment],
}),
512,
);
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(result).toEqual([
{
path: attachment.url,
contentType: "image/png",
placeholder: "<media:image>",
},
]);
});
it("keeps sticker metadata when sticker download fails", async () => {
const sticker = {
id: "sticker-fallback",
name: "fallback",
format_type: StickerFormatType.PNG,
};
fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard"));
const result = await resolveMediaList(
asMessage({
stickers: [sticker],
}),
512,
);
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(result).toEqual([
{
path: "https://media.discordapp.net/stickers/sticker-fallback.png",
contentType: "image/png",
placeholder: "<media:sticker>",
},
]);
});
});
describe("Discord media SSRF policy", () => {

View File

@@ -250,6 +250,12 @@ async function appendResolvedMediaFromAttachments(params: {
} catch (err) {
const id = attachment.id ?? attachment.url;
logVerbose(`${params.errorPrefix} ${id}: ${String(err)}`);
// Preserve attachment context even when remote fetch is blocked/fails.
params.out.push({
path: attachment.url,
contentType: attachment.content_type,
placeholder: inferPlaceholder(attachment),
});
}
}
}
@@ -306,6 +312,19 @@ function formatStickerError(err: unknown): string {
}
}
function inferStickerContentType(sticker: APIStickerItem): string | undefined {
switch (sticker.format_type) {
case StickerFormatType.GIF:
return "image/gif";
case StickerFormatType.APNG:
case StickerFormatType.Lottie:
case StickerFormatType.PNG:
return "image/png";
default:
return undefined;
}
}
async function appendResolvedMediaFromStickers(params: {
stickers?: APIStickerItem[] | null;
maxBytes: number;
@@ -348,6 +367,14 @@ async function appendResolvedMediaFromStickers(params: {
}
if (lastError) {
logVerbose(`${params.errorPrefix} ${sticker.id}: ${formatStickerError(lastError)}`);
const fallback = candidates[0];
if (fallback) {
params.out.push({
path: fallback.url,
contentType: inferStickerContentType(sticker),
placeholder: "<media:sticker>",
});
}
}
}
}