mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 03:04:29 -04:00
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.
This commit is contained in:
@@ -254,9 +254,20 @@ function createMockRequest(
|
|||||||
body: unknown,
|
body: unknown,
|
||||||
headers: Record<string, string> = {},
|
headers: Record<string, string> = {},
|
||||||
): IncomingMessage {
|
): 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;
|
const req = new EventEmitter() as IncomingMessage;
|
||||||
req.method = method;
|
req.method = method;
|
||||||
req.url = url;
|
req.url = `${parsedUrl.pathname}${parsedUrl.search}`;
|
||||||
req.headers = headers;
|
req.headers = headers;
|
||||||
(req as unknown as { socket: { remoteAddress: string } }).socket = { remoteAddress: "127.0.0.1" };
|
(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);
|
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 account = createMockAccount({ password: "secret-token" });
|
||||||
const config: OpenClawConfig = {};
|
const config: OpenClawConfig = {};
|
||||||
const core = createMockRuntime();
|
const core = createMockRuntime();
|
||||||
setBlueBubblesRuntime(core);
|
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", {
|
const loopbackUnregister = registerBlueBubblesWebhookTarget({
|
||||||
type: "new-message",
|
account,
|
||||||
data: {
|
config,
|
||||||
text: "hello",
|
runtime: { log: vi.fn(), error: vi.fn() },
|
||||||
handle: { address: "+15551234567" },
|
core,
|
||||||
isGroup: false,
|
path: "/bluebubbles-webhook",
|
||||||
isFromMe: false,
|
});
|
||||||
guid: "msg-1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Localhost address
|
|
||||||
(req as unknown as { socket: { remoteAddress: string } }).socket = {
|
|
||||||
remoteAddress: "127.0.0.1",
|
|
||||||
};
|
|
||||||
|
|
||||||
unregister = registerBlueBubblesWebhookTarget({
|
const res = createMockResponse();
|
||||||
account,
|
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
||||||
config,
|
expect(handled).toBe(true);
|
||||||
runtime: { log: vi.fn(), error: vi.fn() },
|
expect(res.statusCode).toBe(401);
|
||||||
core,
|
|
||||||
path: "/bluebubbles-webhook",
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = createMockResponse();
|
loopbackUnregister();
|
||||||
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
}
|
||||||
|
|
||||||
expect(handled).toBe(true);
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores unregistered webhook paths", async () => {
|
it("ignores unregistered webhook paths", async () => {
|
||||||
|
|||||||
@@ -1533,10 +1533,6 @@ export async function handleBlueBubblesWebhookRequest(
|
|||||||
if (guid && guid.trim() === token) {
|
if (guid && guid.trim() === token) {
|
||||||
return true;
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user