mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 05:37:27 +00:00
This commit is contained in:
@@ -104,11 +104,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
discordRestFetch,
|
||||
} = ctx;
|
||||
|
||||
const mediaList = await resolveMediaList(message, mediaMaxBytes, discordRestFetch);
|
||||
const ssrfPolicy = cfg.browser?.ssrfPolicy;
|
||||
const mediaList = await resolveMediaList(message, mediaMaxBytes, discordRestFetch, ssrfPolicy);
|
||||
const forwardedMediaList = await resolveForwardedMediaList(
|
||||
message,
|
||||
mediaMaxBytes,
|
||||
discordRestFetch,
|
||||
ssrfPolicy,
|
||||
);
|
||||
mediaList.push(...forwardedMediaList);
|
||||
const text = messageText;
|
||||
|
||||
@@ -30,6 +30,22 @@ function asMessage(payload: Record<string, unknown>): Message {
|
||||
return payload as unknown as Message;
|
||||
}
|
||||
|
||||
const DISCORD_CDN_HOSTNAMES = [
|
||||
"cdn.discordapp.com",
|
||||
"media.discordapp.net",
|
||||
"*.discordapp.com",
|
||||
"*.discordapp.net",
|
||||
];
|
||||
|
||||
function expectDiscordCdnSsrFPolicy(policy: unknown) {
|
||||
expect(policy).toEqual(
|
||||
expect.objectContaining({
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
hostnameAllowlist: expect.arrayContaining(DISCORD_CDN_HOSTNAMES),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function expectSinglePngDownload(params: {
|
||||
result: unknown;
|
||||
expectedUrl: string;
|
||||
@@ -38,13 +54,20 @@ function expectSinglePngDownload(params: {
|
||||
placeholder: "<media:image>" | "<media:sticker>";
|
||||
}) {
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledTimes(1);
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith({
|
||||
const call = fetchRemoteMedia.mock.calls[0]?.[0] as {
|
||||
url?: string;
|
||||
filePathHint?: string;
|
||||
maxBytes?: number;
|
||||
fetchImpl?: unknown;
|
||||
ssrfPolicy?: unknown;
|
||||
};
|
||||
expect(call).toMatchObject({
|
||||
url: params.expectedUrl,
|
||||
filePathHint: params.filePathHint,
|
||||
maxBytes: 512,
|
||||
fetchImpl: undefined,
|
||||
ssrfPolicy: expect.objectContaining({ allowRfc2544BenchmarkRange: true }),
|
||||
});
|
||||
expectDiscordCdnSsrFPolicy(call.ssrfPolicy);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
|
||||
expect(params.result).toEqual([
|
||||
@@ -151,13 +174,20 @@ describe("resolveForwardedMediaList", () => {
|
||||
);
|
||||
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledTimes(1);
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith({
|
||||
const call = fetchRemoteMedia.mock.calls[0]?.[0] as {
|
||||
url?: string;
|
||||
filePathHint?: string;
|
||||
maxBytes?: number;
|
||||
fetchImpl?: unknown;
|
||||
ssrfPolicy?: unknown;
|
||||
};
|
||||
expect(call).toMatchObject({
|
||||
url: attachment.url,
|
||||
filePathHint: attachment.filename,
|
||||
maxBytes: 512,
|
||||
fetchImpl: undefined,
|
||||
ssrfPolicy: expect.objectContaining({ allowRfc2544BenchmarkRange: true }),
|
||||
});
|
||||
expectDiscordCdnSsrFPolicy(call.ssrfPolicy);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
|
||||
expect(result).toEqual([
|
||||
@@ -471,7 +501,7 @@ describe("Discord media SSRF policy", () => {
|
||||
saveMediaBuffer.mockClear();
|
||||
});
|
||||
|
||||
it("passes ssrfPolicy with Discord CDN allowedHostnames and allowRfc2544BenchmarkRange", async () => {
|
||||
it("passes Discord CDN hostname allowlist with RFC2544 enabled", async () => {
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("img"),
|
||||
contentType: "image/png",
|
||||
@@ -488,11 +518,42 @@ describe("Discord media SSRF policy", () => {
|
||||
1024,
|
||||
);
|
||||
|
||||
const policy = fetchRemoteMedia.mock.calls[0][0].ssrfPolicy;
|
||||
expect(policy).toEqual({
|
||||
allowedHostnames: ["cdn.discordapp.com", "media.discordapp.net"],
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
const policy = fetchRemoteMedia.mock.calls[0]?.[0]?.ssrfPolicy;
|
||||
expectDiscordCdnSsrFPolicy(policy);
|
||||
});
|
||||
|
||||
it("merges provided ssrfPolicy with Discord CDN defaults", async () => {
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("img"),
|
||||
contentType: "image/png",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/b.png",
|
||||
contentType: "image/png",
|
||||
});
|
||||
|
||||
await resolveMediaList(
|
||||
asMessage({
|
||||
attachments: [{ id: "b1", url: "https://cdn.discordapp.com/b.png", filename: "b.png" }],
|
||||
}),
|
||||
1024,
|
||||
undefined,
|
||||
{
|
||||
allowPrivateNetwork: true,
|
||||
hostnameAllowlist: ["assets.example.com"],
|
||||
allowedHostnames: ["assets.example.com"],
|
||||
},
|
||||
);
|
||||
|
||||
const policy = fetchRemoteMedia.mock.calls[0]?.[0]?.ssrfPolicy;
|
||||
expect(policy).toEqual(
|
||||
expect.objectContaining({
|
||||
allowPrivateNetwork: true,
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
allowedHostnames: expect.arrayContaining(["assets.example.com"]),
|
||||
hostnameAllowlist: expect.arrayContaining(["assets.example.com", ...DISCORD_CDN_HOSTNAMES]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,11 +6,53 @@ import type { SsrFPolicy } from "../../infra/net/ssrf.js";
|
||||
import { fetchRemoteMedia, type FetchLike } from "../../media/fetch.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
|
||||
const DISCORD_CDN_HOSTNAMES = [
|
||||
"cdn.discordapp.com",
|
||||
"media.discordapp.net",
|
||||
"*.discordapp.com",
|
||||
"*.discordapp.net",
|
||||
];
|
||||
|
||||
// Allow Discord CDN downloads when VPN/proxy DNS resolves to RFC2544 benchmark ranges.
|
||||
const DISCORD_MEDIA_SSRF_POLICY: SsrFPolicy = {
|
||||
allowedHostnames: ["cdn.discordapp.com", "media.discordapp.net"],
|
||||
hostnameAllowlist: DISCORD_CDN_HOSTNAMES,
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
};
|
||||
|
||||
function mergeHostnameList(...lists: Array<string[] | undefined>): string[] | undefined {
|
||||
const merged = lists
|
||||
.flatMap((list) => list ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
if (merged.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Array.from(new Set(merged));
|
||||
}
|
||||
|
||||
function resolveDiscordMediaSsrFPolicy(policy?: SsrFPolicy): SsrFPolicy {
|
||||
if (!policy) {
|
||||
return DISCORD_MEDIA_SSRF_POLICY;
|
||||
}
|
||||
const hostnameAllowlist = mergeHostnameList(
|
||||
DISCORD_MEDIA_SSRF_POLICY.hostnameAllowlist,
|
||||
policy.hostnameAllowlist,
|
||||
);
|
||||
const allowedHostnames = mergeHostnameList(
|
||||
DISCORD_MEDIA_SSRF_POLICY.allowedHostnames,
|
||||
policy.allowedHostnames,
|
||||
);
|
||||
return {
|
||||
...DISCORD_MEDIA_SSRF_POLICY,
|
||||
...policy,
|
||||
...(allowedHostnames ? { allowedHostnames } : {}),
|
||||
...(hostnameAllowlist ? { hostnameAllowlist } : {}),
|
||||
allowRfc2544BenchmarkRange:
|
||||
Boolean(DISCORD_MEDIA_SSRF_POLICY.allowRfc2544BenchmarkRange) ||
|
||||
Boolean(policy.allowRfc2544BenchmarkRange),
|
||||
};
|
||||
}
|
||||
|
||||
export type DiscordMediaInfo = {
|
||||
path: string;
|
||||
contentType?: string;
|
||||
@@ -168,14 +210,17 @@ export async function resolveMediaList(
|
||||
message: Message,
|
||||
maxBytes: number,
|
||||
fetchImpl?: FetchLike,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
): Promise<DiscordMediaInfo[]> {
|
||||
const out: DiscordMediaInfo[] = [];
|
||||
const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(ssrfPolicy);
|
||||
await appendResolvedMediaFromAttachments({
|
||||
attachments: message.attachments ?? [],
|
||||
maxBytes,
|
||||
out,
|
||||
errorPrefix: "discord: failed to download attachment",
|
||||
fetchImpl,
|
||||
ssrfPolicy: resolvedSsrFPolicy,
|
||||
});
|
||||
await appendResolvedMediaFromStickers({
|
||||
stickers: resolveDiscordMessageStickers(message),
|
||||
@@ -183,6 +228,7 @@ export async function resolveMediaList(
|
||||
out,
|
||||
errorPrefix: "discord: failed to download sticker",
|
||||
fetchImpl,
|
||||
ssrfPolicy: resolvedSsrFPolicy,
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -191,12 +237,14 @@ export async function resolveForwardedMediaList(
|
||||
message: Message,
|
||||
maxBytes: number,
|
||||
fetchImpl?: FetchLike,
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
): Promise<DiscordMediaInfo[]> {
|
||||
const snapshots = resolveDiscordMessageSnapshots(message);
|
||||
if (snapshots.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const out: DiscordMediaInfo[] = [];
|
||||
const resolvedSsrFPolicy = resolveDiscordMediaSsrFPolicy(ssrfPolicy);
|
||||
for (const snapshot of snapshots) {
|
||||
await appendResolvedMediaFromAttachments({
|
||||
attachments: snapshot.message?.attachments,
|
||||
@@ -204,6 +252,7 @@ export async function resolveForwardedMediaList(
|
||||
out,
|
||||
errorPrefix: "discord: failed to download forwarded attachment",
|
||||
fetchImpl,
|
||||
ssrfPolicy: resolvedSsrFPolicy,
|
||||
});
|
||||
await appendResolvedMediaFromStickers({
|
||||
stickers: snapshot.message ? resolveDiscordSnapshotStickers(snapshot.message) : [],
|
||||
@@ -211,6 +260,7 @@ export async function resolveForwardedMediaList(
|
||||
out,
|
||||
errorPrefix: "discord: failed to download forwarded sticker",
|
||||
fetchImpl,
|
||||
ssrfPolicy: resolvedSsrFPolicy,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@@ -222,6 +272,7 @@ async function appendResolvedMediaFromAttachments(params: {
|
||||
out: DiscordMediaInfo[];
|
||||
errorPrefix: string;
|
||||
fetchImpl?: FetchLike;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}) {
|
||||
const attachments = params.attachments;
|
||||
if (!attachments || attachments.length === 0) {
|
||||
@@ -234,7 +285,7 @@ async function appendResolvedMediaFromAttachments(params: {
|
||||
filePathHint: attachment.filename ?? attachment.url,
|
||||
maxBytes: params.maxBytes,
|
||||
fetchImpl: params.fetchImpl,
|
||||
ssrfPolicy: DISCORD_MEDIA_SSRF_POLICY,
|
||||
ssrfPolicy: params.ssrfPolicy,
|
||||
});
|
||||
const saved = await saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
@@ -331,6 +382,7 @@ async function appendResolvedMediaFromStickers(params: {
|
||||
out: DiscordMediaInfo[];
|
||||
errorPrefix: string;
|
||||
fetchImpl?: FetchLike;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}) {
|
||||
const stickers = params.stickers;
|
||||
if (!stickers || stickers.length === 0) {
|
||||
@@ -346,7 +398,7 @@ async function appendResolvedMediaFromStickers(params: {
|
||||
filePathHint: candidate.fileName,
|
||||
maxBytes: params.maxBytes,
|
||||
fetchImpl: params.fetchImpl,
|
||||
ssrfPolicy: DISCORD_MEDIA_SSRF_POLICY,
|
||||
ssrfPolicy: params.ssrfPolicy,
|
||||
});
|
||||
const saved = await saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
|
||||
Reference in New Issue
Block a user