fix(qqbot): guard image-size probe against SSRF (#63495)

* fix(qqbot): replace raw fetch in image-size probe with SSRF-guarded fetchRemoteMedia

Replace the bare fetch() in getImageSizeFromUrl() with fetchRemoteMedia()
from the plugin SDK, closing the blind SSRF via markdown image dimension
probing (GHSA-2767-2q9v-9326).

fetchRemoteMedia options: maxBytes 65536, maxRedirects 0, generic
public-network-only SSRF policy (no hostname allowlist, blocks
private/reserved/loopback/link-local/metadata IPs after DNS resolution).

Also fixes the repo-root resolution in scripts/lib/ts-guard-utils.mjs
which caused lint:tmp:no-raw-channel-fetch to miss extension files
entirely. The guard now walks up to .git instead of hardcoding two parent
traversals, and the allowlist is refreshed with all pre-existing raw
fetch callsites that became visible.

* fix(qqbot): guard image-size probe against SSRF (#63495) (thanks @dims)

---------

Co-authored-by: sliverp <870080352@qq.com>
This commit is contained in:
Davanum Srinivas
2026-04-09 10:48:04 +02:00
committed by GitHub
parent ab49afcd27
commit 08ae021d1f
7 changed files with 378 additions and 63 deletions

View File

@@ -14,41 +14,61 @@ const sourceRoots = ["src/channels", "src/routing", "src/line", "extensions"];
// Temporary allowlist for legacy callsites. New raw fetch callsites in channel/plugin runtime
// code should be rejected and migrated to fetchWithSsrFGuard/shared channel helpers.
const allowedRawFetchCallsites = new Set([
bundledPluginCallsite("bluebubbles", "src/types.ts", 133),
bundledPluginCallsite("feishu", "src/streaming-card.ts", 31),
bundledPluginCallsite("feishu", "src/streaming-card.ts", 101),
bundledPluginCallsite("feishu", "src/streaming-card.ts", 143),
bundledPluginCallsite("feishu", "src/streaming-card.ts", 199),
bundledPluginCallsite("googlechat", "src/api.ts", 22),
bundledPluginCallsite("googlechat", "src/api.ts", 43),
bundledPluginCallsite("googlechat", "src/api.ts", 63),
bundledPluginCallsite("googlechat", "src/api.ts", 188),
bundledPluginCallsite("googlechat", "src/auth.ts", 82),
bundledPluginCallsite("matrix", "src/directory-live.ts", 41),
bundledPluginCallsite("matrix", "src/matrix/client/config.ts", 171),
bundledPluginCallsite("mattermost", "src/mattermost/client.ts", 211),
bundledPluginCallsite("mattermost", "src/mattermost/monitor.ts", 230),
bundledPluginCallsite("mattermost", "src/mattermost/probe.ts", 27),
bundledPluginCallsite("minimax", "oauth.ts", 62),
bundledPluginCallsite("minimax", "oauth.ts", 93),
bundledPluginCallsite("msteams", "src/graph.ts", 39),
bundledPluginCallsite("nextcloud-talk", "src/room-info.ts", 92),
bundledPluginCallsite("nextcloud-talk", "src/send.ts", 107),
bundledPluginCallsite("nextcloud-talk", "src/send.ts", 198),
bundledPluginCallsite("talk-voice", "index.ts", 27),
bundledPluginCallsite("thread-ownership", "index.ts", 105),
bundledPluginCallsite("voice-call", "src/providers/plivo.ts", 95),
bundledPluginCallsite("voice-call", "src/providers/telnyx.ts", 61),
bundledPluginCallsite("voice-call", "src/providers/tts-openai.ts", 111),
bundledPluginCallsite("bluebubbles", "src/test-harness.ts", 128),
bundledPluginCallsite("bluebubbles", "src/types.ts", 181),
bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 235),
bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 217),
bundledPluginCallsite("browser", "src/browser/test-fetch.ts", 24),
bundledPluginCallsite("browser", "src/browser/test-fetch.ts", 27),
bundledPluginCallsite("chutes", "models.ts", 535),
bundledPluginCallsite("chutes", "models.ts", 542),
bundledPluginCallsite("discord", "src/monitor/gateway-plugin.ts", 322),
bundledPluginCallsite("discord", "src/monitor/gateway-plugin.ts", 360),
bundledPluginCallsite("discord", "src/voice-message.ts", 298),
bundledPluginCallsite("discord", "src/voice-message.ts", 333),
bundledPluginCallsite("elevenlabs", "speech-provider.ts", 295),
bundledPluginCallsite("elevenlabs", "tts.ts", 116),
bundledPluginCallsite("feishu", "src/monitor.webhook.test-helpers.ts", 25),
bundledPluginCallsite("github-copilot", "login.ts", 48),
bundledPluginCallsite("github-copilot", "login.ts", 80),
bundledPluginCallsite("googlechat", "src/auth.ts", 83),
bundledPluginCallsite("huggingface", "models.ts", 142),
bundledPluginCallsite("kilocode", "provider-models.ts", 130),
bundledPluginCallsite("matrix", "src/matrix/sdk/transport.ts", 112),
bundledPluginCallsite("microsoft-foundry", "onboard.ts", 479),
bundledPluginCallsite("microsoft", "speech-provider.ts", 132),
bundledPluginCallsite("minimax", "oauth.ts", 66),
bundledPluginCallsite("minimax", "oauth.ts", 107),
bundledPluginCallsite("minimax", "tts.ts", 52),
bundledPluginCallsite("msteams", "src/graph.ts", 47),
bundledPluginCallsite("msteams", "src/sdk.ts", 292),
bundledPluginCallsite("msteams", "src/sdk.ts", 333),
bundledPluginCallsite("ollama", "src/stream.ts", 649),
bundledPluginCallsite("openai", "tts.ts", 133),
bundledPluginCallsite("qa-channel", "src/bus-client.ts", 41),
bundledPluginCallsite("qa-channel", "src/bus-client.ts", 221),
bundledPluginCallsite("qa-lab", "src/docker-up.runtime.ts", 274),
bundledPluginCallsite("qa-lab", "src/gateway-child.ts", 488),
bundledPluginCallsite("qa-lab", "src/suite.ts", 330),
bundledPluginCallsite("qa-lab", "src/suite.ts", 341),
bundledPluginCallsite("qa-lab", "web/src/app.ts", 15),
bundledPluginCallsite("qa-lab", "web/src/app.ts", 23),
bundledPluginCallsite("qa-lab", "web/src/app.ts", 31),
bundledPluginCallsite("qqbot", "src/api.ts", 102),
bundledPluginCallsite("qqbot", "src/api.ts", 237),
bundledPluginCallsite("qqbot", "src/stt.ts", 81),
bundledPluginCallsite("qqbot", "src/tools/channel.ts", 180),
bundledPluginCallsite("qqbot", "src/utils/audio-convert.ts", 377),
bundledPluginCallsite("signal", "src/install-signal-cli.ts", 224),
bundledPluginCallsite("slack", "src/monitor/media.ts", 96),
bundledPluginCallsite("slack", "src/monitor/media.ts", 115),
bundledPluginCallsite("slack", "src/monitor/media.ts", 120),
bundledPluginCallsite("tlon", "src/tlon-api.ts", 185),
bundledPluginCallsite("tlon", "src/tlon-api.ts", 235),
bundledPluginCallsite("tlon", "src/tlon-api.ts", 289),
bundledPluginCallsite("venice", "models.ts", 552),
bundledPluginCallsite("vercel-ai-gateway", "models.ts", 181),
bundledPluginCallsite("voice-call", "src/providers/twilio/api.ts", 23),
bundledPluginCallsite("telegram", "src/api-fetch.ts", 8),
bundledPluginCallsite("discord", "src/send.outbound.ts", 363),
bundledPluginCallsite("discord", "src/voice-message.ts", 268),
bundledPluginCallsite("discord", "src/voice-message.ts", 312),
bundledPluginCallsite("slack", "src/monitor/media.ts", 55),
bundledPluginCallsite("slack", "src/monitor/media.ts", 59),
bundledPluginCallsite("slack", "src/monitor/media.ts", 73),
bundledPluginCallsite("slack", "src/monitor/media.ts", 99),
]);
function isRawFetchCall(expression) {

View File

@@ -1,4 +1,4 @@
import { promises as fs } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -14,6 +14,18 @@ function getTypeScript() {
const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"];
export function resolveRepoRoot(importMetaUrl) {
// Walk up from the caller's directory until we find the repo root (.git).
// This handles callers at any depth (scripts/*.mjs, scripts/lib/*.mjs, etc.)
// instead of assuming a fixed number of parent traversals.
let dir = path.dirname(fileURLToPath(importMetaUrl));
const { root } = path.parse(dir);
while (dir !== root) {
if (existsSync(path.join(dir, ".git"))) {
return dir;
}
dir = path.dirname(dir);
}
// Fallback: two levels up (original behavior).
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
}