mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05: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,
|
||||
headers: Record<string, string> = {},
|
||||
): 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 () => {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user