mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 07:57:40 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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)), "..", "..");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user