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 <steipete@gmail.com>
This commit is contained in:
xingzhou
2026-08-26 07:36:24 +08:00
committed by GitHub
parent 574f7536c8
commit b0526f7be7
9 changed files with 76 additions and 33 deletions
-1
View File
@@ -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
@@ -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",
}),
})),
),
);
});
@@ -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",
}),
})),
),
);
});
+56 -4
View File
@@ -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<string>();
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<Uint8Array>({
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<typeof fetch>(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<typeof fetch>(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");
@@ -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",
}),
})),
),
);
});
+3 -4
View File
@@ -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(
(
+3 -4
View File
@@ -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",
}),
})),
),
);
});
+2 -4
View File
@@ -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)) {
+3 -4
View File
@@ -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",
}),
})),
),
);
});