diff --git a/CHANGELOG.md b/CHANGELOG.md index 09491b3b9..37bf902c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Security: guard skill installer downloads with SSRF checks (block private/localhost URLs). - Media understanding: apply SSRF guardrails to provider fetches; allow private baseUrl overrides explicitly. +- Tests: stub SSRF DNS pinning in web auto-reply + Gemini video coverage. (#6619) Thanks @joshp123. ## 2026.2.1 diff --git a/src/media-understanding/providers/google/video.test.ts b/src/media-understanding/providers/google/video.test.ts index 4d05a8e04..20675d4a3 100644 --- a/src/media-understanding/providers/google/video.test.ts +++ b/src/media-understanding/providers/google/video.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as ssrf from "../../../infra/net/ssrf.js"; import { describeGeminiVideo } from "./video.js"; +const TEST_NET_IP = "203.0.113.10"; + const resolveRequestUrl = (input: RequestInfo | URL) => { if (typeof input === "string") { return input; @@ -12,6 +15,28 @@ const resolveRequestUrl = (input: RequestInfo | URL) => { }; describe("describeGeminiVideo", () => { + let resolvePinnedHostnameSpy: ReturnType | undefined; + + beforeEach(() => { + resolvePinnedHostnameSpy = vi + .spyOn(ssrf, "resolvePinnedHostnameWithPolicy") + .mockImplementation(async (hostname) => { + // SSRF guard pins DNS; stub resolution to avoid live lookups in unit tests. + const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); + const addresses = [TEST_NET_IP]; + return { + hostname: normalized, + addresses, + lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }), + }; + }); + }); + + afterEach(() => { + resolvePinnedHostnameSpy?.mockRestore(); + resolvePinnedHostnameSpy = undefined; + }); + it("respects case-insensitive x-goog-api-key overrides", async () => { let seenKey: string | null = null; const fetchFn = async (_input: RequestInfo | URL, init?: RequestInit) => { diff --git a/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts b/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts index 60d9fb83e..e9f078cff 100644 --- a/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +++ b/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts @@ -5,6 +5,9 @@ import os from "node:os"; import path from "node:path"; import sharp from "sharp"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as ssrf from "../infra/net/ssrf.js"; + +const TEST_NET_IP = "203.0.113.10"; vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), @@ -95,13 +98,29 @@ const _makeSessionStore = async ( }; describe("web auto-reply", () => { + let resolvePinnedHostnameSpy: ReturnType | undefined; + beforeEach(() => { vi.clearAllMocks(); resetBaileysMocks(); resetLoadConfigMock(); + resolvePinnedHostnameSpy = vi + .spyOn(ssrf, "resolvePinnedHostname") + .mockImplementation(async (hostname) => { + // SSRF guard pins DNS; stub resolution to avoid live lookups in unit tests. + const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); + const addresses = [TEST_NET_IP]; + return { + hostname: normalized, + addresses, + lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }), + }; + }); }); afterEach(() => { + resolvePinnedHostnameSpy?.mockRestore(); + resolvePinnedHostnameSpy = undefined; resetLogger(); setLoggerOverride(null); vi.useRealTimers(); diff --git a/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts b/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts index c1bc72d6f..4e158e1ca 100644 --- a/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts +++ b/src/web/auto-reply.web-auto-reply.falls-back-text-media-send-fails.test.ts @@ -4,6 +4,9 @@ import os from "node:os"; import path from "node:path"; import sharp from "sharp"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as ssrf from "../infra/net/ssrf.js"; + +const TEST_NET_IP = "203.0.113.10"; vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), @@ -94,13 +97,29 @@ const _makeSessionStore = async ( }; describe("web auto-reply", () => { + let resolvePinnedHostnameSpy: ReturnType | undefined; + beforeEach(() => { vi.clearAllMocks(); resetBaileysMocks(); resetLoadConfigMock(); + resolvePinnedHostnameSpy = vi + .spyOn(ssrf, "resolvePinnedHostname") + .mockImplementation(async (hostname) => { + // SSRF guard pins DNS; stub resolution to avoid live lookups in unit tests. + const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); + const addresses = [TEST_NET_IP]; + return { + hostname: normalized, + addresses, + lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }), + }; + }); }); afterEach(() => { + resolvePinnedHostnameSpy?.mockRestore(); + resolvePinnedHostnameSpy = undefined; resetLogger(); setLoggerOverride(null); vi.useRealTimers();