From b0526f7be7f15e302a3a4d72a97191ef8e515f82 Mon Sep 17 00:00:00 2001 From: xingzhou Date: Wed, 26 Aug 2026 07:36:24 +0800 Subject: [PATCH] fix(buzz): bound relay information responses (#119182) Reject malformed relay metadata and cancel oversized response streams at the existing 16 MiB provider boundary. Co-authored-by: Peter Steinberger --- config/assertion-safety-baseline.txt | 1 - .../buzz/src/buzz-bus.archived-rooms.test.ts | 7 +-- .../buzz/src/buzz-bus.history-catchup.test.ts | 7 +-- .../buzz/src/buzz-bus.lifecycle.test.ts | 60 +++++++++++++++++-- extensions/buzz/src/buzz-bus.mentions.test.ts | 7 +-- extensions/buzz/src/directory.test.ts | 7 +-- extensions/buzz/src/qa/relay-client.test.ts | 7 +-- extensions/buzz/src/relay-auth.ts | 6 +- extensions/buzz/src/room-discovery.test.ts | 7 +-- 9 files changed, 76 insertions(+), 33 deletions(-) diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 3a43afeacae2..3b2540e1f4ab 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -133,7 +133,6 @@ extensions/buzz/src/gateway.ts 1 extensions/buzz/src/inbound.ts 1 extensions/buzz/src/message-event.ts 2 extensions/buzz/src/qa/adapter.runtime.ts 2 -extensions/buzz/src/relay-auth.ts 1 extensions/buzz/src/room-membership.ts 2 extensions/buzz/src/setup-core.ts 1 extensions/buzz/src/setup-surface.ts 1 diff --git a/extensions/buzz/src/buzz-bus.archived-rooms.test.ts b/extensions/buzz/src/buzz-bus.archived-rooms.test.ts index 3907eb2d2ea5..e92d150b47ef 100644 --- a/extensions/buzz/src/buzz-bus.archived-rooms.test.ts +++ b/extensions/buzz/src/buzz-bus.archived-rooms.test.ts @@ -109,13 +109,12 @@ describe("Buzz archived room lifecycle", () => { relayMocks.roomMetadataEvents = []; vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); }); diff --git a/extensions/buzz/src/buzz-bus.history-catchup.test.ts b/extensions/buzz/src/buzz-bus.history-catchup.test.ts index 94229514e444..efdda71fe8f9 100644 --- a/extensions/buzz/src/buzz-bus.history-catchup.test.ts +++ b/extensions/buzz/src/buzz-bus.history-catchup.test.ts @@ -204,13 +204,12 @@ describe("Buzz reconnect history catch-up", () => { relayMocks.connected = true; vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); }); diff --git a/extensions/buzz/src/buzz-bus.lifecycle.test.ts b/extensions/buzz/src/buzz-bus.lifecycle.test.ts index cbb3bc88382a..5fed6f14c157 100644 --- a/extensions/buzz/src/buzz-bus.lifecycle.test.ts +++ b/extensions/buzz/src/buzz-bus.lifecycle.test.ts @@ -127,6 +127,7 @@ const BOT_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(PRIVATE_KEY, "he const SENDER_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex"))); const SENDER_SECRET_KEY = Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex")); const RELAY_PUBLIC_KEY = "f".repeat(64); +const BUZZ_RELAY_INFO_MAX_BYTES = 16 * 1024 * 1024; const tempDirs = new Set(); let previousStateDir: string | undefined; let stateDir: string; @@ -209,13 +210,12 @@ describe("Buzz bus lifecycle", () => { relayMocks.stallRoomEoseChannelId = undefined; vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); }); @@ -291,6 +291,58 @@ describe("Buzz bus lifecycle", () => { vi.useRealTimers(); }); + it("cancels oversized NIP-11 relay information before consuming the entire response", async () => { + relayMocks.auth.mockResolvedValue("ok"); + const cancel = vi.fn(); + const chunk = new Uint8Array(1024 * 1024).fill("x".charCodeAt(0)); + let emittedChunks = 0; + const body = new ReadableStream({ + pull(controller) { + if (emittedChunks === 0) { + controller.enqueue( + new TextEncoder().encode(`{"self":"${RELAY_PUBLIC_KEY}","description":"`), + ); + } else if (emittedChunks <= 17) { + controller.enqueue(chunk); + } else { + controller.enqueue(new TextEncoder().encode('"}')); + controller.close(); + } + emittedChunks += 1; + }, + cancel, + }); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(body)), + ); + + await expect(startTestBus()).rejects.toThrow( + `Buzz relay information: JSON response exceeds ${BUZZ_RELAY_INFO_MAX_BYTES} bytes`, + ); + + expect(cancel).toHaveBeenCalledOnce(); + expect(emittedChunks).toBeLessThan(19); + expect(relayMocks.close).toHaveBeenCalledOnce(); + }); + + it.each([ + ["truncated JSON", '{"self":'], + ["null", "null"], + ["an array", "[]"], + ["a primitive", "true"], + ])("rejects malformed NIP-11 relay information containing %s", async (_label, body) => { + relayMocks.auth.mockResolvedValue("ok"); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(body)), + ); + + await expect(startTestBus()).rejects.toThrow("Buzz relay information: malformed JSON response"); + + expect(relayMocks.close).toHaveBeenCalledOnce(); + }); + it("publishes and closes a standalone authenticated send", async () => { relayMocks.auth.mockResolvedValue("ok"); diff --git a/extensions/buzz/src/buzz-bus.mentions.test.ts b/extensions/buzz/src/buzz-bus.mentions.test.ts index 3a5ca34f7329..6f801e47be7e 100644 --- a/extensions/buzz/src/buzz-bus.mentions.test.ts +++ b/extensions/buzz/src/buzz-bus.mentions.test.ts @@ -118,13 +118,12 @@ describe("Buzz mention delivery", () => { relayMocks.send.mockResolvedValue(); vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); }); diff --git a/extensions/buzz/src/directory.test.ts b/extensions/buzz/src/directory.test.ts index a0497700c441..a6e5737bd007 100644 --- a/extensions/buzz/src/directory.test.ts +++ b/extensions/buzz/src/directory.test.ts @@ -88,13 +88,12 @@ describe("Buzz live directory", () => { gatewayMocks.activeBus = undefined; vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); relayMocks.subscribe.mockImplementation( ( diff --git a/extensions/buzz/src/qa/relay-client.test.ts b/extensions/buzz/src/qa/relay-client.test.ts index f261beea8bb6..198b484cc7b8 100644 --- a/extensions/buzz/src/qa/relay-client.test.ts +++ b/extensions/buzz/src/qa/relay-client.test.ts @@ -89,13 +89,12 @@ describe("Buzz QA relay driver", () => { relayMocks.replayedMessage = undefined; vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); }); diff --git a/extensions/buzz/src/relay-auth.ts b/extensions/buzz/src/relay-auth.ts index 4ccd97e7a8a9..0de32678f6e4 100644 --- a/extensions/buzz/src/relay-auth.ts +++ b/extensions/buzz/src/relay-auth.ts @@ -1,4 +1,5 @@ import { type EventTemplate, finalizeEvent, Relay, type VerifiedEvent } from "nostr-tools"; +import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedOrigin, @@ -91,10 +92,7 @@ async function resolveBuzzRelayPublicKey(params: { await response.body?.cancel().catch(() => undefined); throw new Error(`Buzz relay information request failed with HTTP ${response.status}`); } - const document = (await response.json()) as { - self?: unknown; - software?: unknown; - }; + const document = await readProviderJsonObjectResponse(response, "Buzz relay information"); const relayPublicKey = typeof document.self === "string" ? document.self.trim().toLowerCase() : ""; if (HEX_PUBLIC_KEY_PATTERN.test(relayPublicKey)) { diff --git a/extensions/buzz/src/room-discovery.test.ts b/extensions/buzz/src/room-discovery.test.ts index a397787fb564..4703d019c46c 100644 --- a/extensions/buzz/src/room-discovery.test.ts +++ b/extensions/buzz/src/room-discovery.test.ts @@ -68,13 +68,12 @@ describe("discoverBuzzRooms", () => { relayMocks.subscribe.mockReset(); vi.stubGlobal( "fetch", - vi.fn(async () => ({ - ok: true, - json: async () => ({ + vi.fn(async () => + Response.json({ self: RELAY_PUBLIC_KEY, software: "https://github.com/block/buzz", }), - })), + ), ); });