Skip to content

Commit 223ae42

Browse files
committed
fix(feishu): harden webhook signature compare
1 parent 2bbf33a commit 223ae42

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
7272
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly. (#47968) Thanks @Takhoffman.
7373
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
7474
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied. (#47968) Thanks @Takhoffman.
75+
- Feishu/webhooks: harden signed webhook verification to use constant-time signature comparison and keep malformed short signatures fail-closed in webhook E2E coverage.
7576
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) Thanks @MonkeyLeeT.
7677
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
7778
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.

extensions/feishu/src/monitor.transport.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ function isFeishuWebhookPayload(value: unknown): value is Record<string, unknown
3232
return !!value && typeof value === "object" && !Array.isArray(value);
3333
}
3434

35+
function timingSafeEqualString(left: string, right: string): boolean {
36+
const leftBuffer = Buffer.from(left, "utf8");
37+
const rightBuffer = Buffer.from(right, "utf8");
38+
if (leftBuffer.length !== rightBuffer.length) {
39+
return false;
40+
}
41+
return crypto.timingSafeEqual(leftBuffer, rightBuffer);
42+
}
43+
3544
function buildFeishuWebhookEnvelope(
3645
req: http.IncomingMessage,
3746
payload: Record<string, unknown>,
@@ -63,7 +72,7 @@ function isFeishuWebhookSignatureValid(params: {
6372
.createHash("sha256")
6473
.update(timestamp + nonce + encryptKey + JSON.stringify(params.payload))
6574
.digest("hex");
66-
return computedSignature === signature;
75+
return timingSafeEqualString(computedSignature, signature);
6776
}
6877

6978
function respondText(res: http.ServerResponse, statusCode: number, body: string): void {

extensions/feishu/src/monitor.webhook-e2e.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@ describe("Feishu webhook signed-request e2e", () => {
114114
);
115115
});
116116

117+
it("rejects malformed short signatures with 401", async () => {
118+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
119+
120+
await withRunningWebhookMonitor(
121+
{
122+
accountId: "short-signature",
123+
path: "/hook-e2e-short-signature",
124+
verificationToken: "verify_token",
125+
encryptKey: "encrypt_key",
126+
},
127+
monitorFeishuProvider,
128+
async (url) => {
129+
const payload = { type: "url_verification", challenge: "challenge-token" };
130+
const headers = signFeishuPayload({ encryptKey: "encrypt_key", payload });
131+
headers["x-lark-signature"] = headers["x-lark-signature"].slice(0, 12);
132+
133+
const response = await fetch(url, {
134+
method: "POST",
135+
headers,
136+
body: JSON.stringify(payload),
137+
});
138+
139+
expect(response.status).toBe(401);
140+
expect(await response.text()).toBe("Invalid signature");
141+
},
142+
);
143+
});
144+
117145
it("returns 400 for invalid json before invoking the sdk", async () => {
118146
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
119147

0 commit comments

Comments
 (0)