From f6504ceb1d2ae7a8a00339ce3b3e9a3685dc8f7e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 24 Apr 2026 16:31:55 -0700 Subject: [PATCH] fix(discord): guard gateway metadata fetches --- .../discord/src/monitor/gateway-plugin.ts | 37 +++++++++++++++++-- .../src/monitor/provider.proxy.test.ts | 15 ++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/extensions/discord/src/monitor/gateway-plugin.ts b/extensions/discord/src/monitor/gateway-plugin.ts index e7d8e4e7d70..1f09c1ad3fe 100644 --- a/extensions/discord/src/monitor/gateway-plugin.ts +++ b/extensions/discord/src/monitor/gateway-plugin.ts @@ -12,6 +12,7 @@ import { } from "openclaw/plugin-sdk/proxy-capture"; import { danger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import * as undici from "undici"; import * as ws from "ws"; @@ -19,6 +20,7 @@ import { validateDiscordProxyUrl } from "../proxy-fetch.js"; import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js"; const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot"; +const DISCORD_API_HOST = "discord.com"; const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/"; const DISCORD_GATEWAY_INFO_TIMEOUT_MS = 10_000; @@ -40,6 +42,25 @@ type CarbonGatewayRegistrationState = { isConnecting?: boolean; }; +function resolveFetchInputUrl(input: RequestInfo | URL): string { + if (typeof input === "string") { + return input; + } + if (input instanceof URL) { + return input.toString(); + } + return input.url; +} + +async function materializeGuardedResponse(response: Response): Promise { + const body = await response.arrayBuffer(); + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +} + function assignCarbonGatewayClient( plugin: carbonGateway.GatewayPlugin, client: Parameters[0], @@ -411,11 +432,19 @@ async function fetchDiscordGatewayMetadataDirect( init?: DiscordGatewayFetchInit, capture?: false | { flowId: string; meta: Record }, ): Promise { - const runtimeFetch = globalThis.fetch; - if (typeof runtimeFetch !== "function") { - throw new Error("fetch is not available"); + const guarded = await fetchWithSsrFGuard({ + url: resolveFetchInputUrl(input), + init: init as RequestInit, + policy: { allowedHostnames: [DISCORD_API_HOST] }, + capture: false, + auditContext: "discord.gateway.metadata", + }); + let response: Response; + try { + response = await materializeGuardedResponse(guarded.response); + } finally { + await guarded.release(); } - const response = await runtimeFetch(input, init as RequestInit); if (capture) { captureHttpExchange({ url: input, diff --git a/extensions/discord/src/monitor/provider.proxy.test.ts b/extensions/discord/src/monitor/provider.proxy.test.ts index e6e8a85f87c..2fd1f11c49a 100644 --- a/extensions/discord/src/monitor/provider.proxy.test.ts +++ b/extensions/discord/src/monitor/provider.proxy.test.ts @@ -138,6 +138,21 @@ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({ resolveDebugProxySettings: resolveDebugProxySettingsMock, })); +vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ + fetchWithSsrFGuard: vi.fn(async (params: { url: string; init?: RequestInit }) => { + const source = (await globalFetchMock(params.url, params.init)) as Response; + const body = await source.text(); + return { + response: new Response(body, { + status: source.status, + statusText: source.statusText, + headers: source.headers, + }), + release: vi.fn(), + }; + }), +})); + describe("createDiscordGatewayPlugin", () => { let createDiscordGatewayPlugin: typeof import("./gateway-plugin.js").createDiscordGatewayPlugin; let waitForDiscordGatewayPluginRegistration: typeof import("./gateway-plugin.js").waitForDiscordGatewayPluginRegistration;