From f836c385ffc746cb954e8ee409f99d079bfdcd2f Mon Sep 17 00:00:00 2001 From: Coy Geek <65363919+coygeek@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:12:17 -0800 Subject: [PATCH] fix: BlueBubbles webhook auth bypass via loopback proxy trust (#13787) * fix(an-08): apply security fix Generated by staged fix workflow. * fix(an-08): apply security fix Generated by staged fix workflow. * fix(an-08): stabilize bluebubbles auth fixture for security patch Restore the default test password in createMockAccount and add a fallback password query in createMockRequest when auth is omitted. This keeps the AN-08 loopback-auth regression tests strict while preserving existing monitor behavior tests that assume authenticated webhook fixtures. --- extensions/bluebubbles/src/monitor.test.ts | 68 +++++++++++++--------- extensions/bluebubbles/src/monitor.ts | 4 -- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index b72a492bd4..a1b3c843be 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -254,9 +254,20 @@ function createMockRequest( body: unknown, headers: Record = {}, ): IncomingMessage { + const parsedUrl = new URL(url, "http://localhost"); + const hasAuthQuery = parsedUrl.searchParams.has("guid") || parsedUrl.searchParams.has("password"); + const hasAuthHeader = + headers["x-guid"] !== undefined || + headers["x-password"] !== undefined || + headers["x-bluebubbles-guid"] !== undefined || + headers.authorization !== undefined; + if (!hasAuthQuery && !hasAuthHeader) { + parsedUrl.searchParams.set("password", "test-password"); + } + const req = new EventEmitter() as IncomingMessage; req.method = method; - req.url = url; + req.url = `${parsedUrl.pathname}${parsedUrl.search}`; req.headers = headers; (req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "127.0.0.1" }; @@ -546,40 +557,41 @@ describe("BlueBubbles webhook monitor", () => { expect(res.statusCode).toBe(401); }); - it("allows localhost requests without authentication", async () => { + it("requires authentication for loopback requests when password is configured", async () => { const account = createMockAccount({ password: "secret-token" }); const config: OpenClawConfig = {}; const core = createMockRuntime(); setBlueBubblesRuntime(core); + for (const remoteAddress of ["127.0.0.1", "::1", "::ffff:127.0.0.1"]) { + const req = createMockRequest("POST", "/bluebubbles-webhook", { + type: "new-message", + data: { + text: "hello", + handle: { address: "+15551234567" }, + isGroup: false, + isFromMe: false, + guid: "msg-1", + }, + }); + (req as unknown as { socket: { remoteAddress: string } }).socket = { + remoteAddress, + }; - const req = createMockRequest("POST", "/bluebubbles-webhook", { - type: "new-message", - data: { - text: "hello", - handle: { address: "+15551234567" }, - isGroup: false, - isFromMe: false, - guid: "msg-1", - }, - }); - // Localhost address - (req as unknown as { socket: { remoteAddress: string } }).socket = { - remoteAddress: "127.0.0.1", - }; + const loopbackUnregister = registerBlueBubblesWebhookTarget({ + account, + config, + runtime: { log: vi.fn(), error: vi.fn() }, + core, + path: "/bluebubbles-webhook", + }); - unregister = registerBlueBubblesWebhookTarget({ - account, - config, - runtime: { log: vi.fn(), error: vi.fn() }, - core, - path: "/bluebubbles-webhook", - }); + const res = createMockResponse(); + const handled = await handleBlueBubblesWebhookRequest(req, res); + expect(handled).toBe(true); + expect(res.statusCode).toBe(401); - const res = createMockResponse(); - const handled = await handleBlueBubblesWebhookRequest(req, res); - - expect(handled).toBe(true); - expect(res.statusCode).toBe(200); + loopbackUnregister(); + } }); it("ignores unregistered webhook paths", async () => { diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index e33b43c69c..bc325b48da 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -1533,10 +1533,6 @@ export async function handleBlueBubblesWebhookRequest( if (guid && guid.trim() === token) { return true; } - const remote = req.socket?.remoteAddress ?? ""; - if (remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1") { - return true; - } return false; });