From fcc9fd162338a600ff30174eca2f633ef9b24865 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 29 Mar 2026 00:42:33 +0000 Subject: [PATCH] fix: land LINE timing-safe signature validation (#55663) (thanks @gavyngong) --- CHANGELOG.md | 1 + extensions/line/src/signature.test.ts | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 extensions/line/src/signature.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1139f0f5ec3..5b64b9be440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - macOS/local gateway: stop OpenClaw.app from killing healthy local gateway listeners after startup by recognizing the current `openclaw-gateway` process title and using the current `openclaw gateway` launch shape. - Memory/QMD: resolve slugified `memory_search` file hints back to the indexed filesystem path before returning search hits, so `memory_get` works again for mixed-case and spaced paths. (#50313) Thanks @erra9x. +- Security/LINE: make webhook signature validation run the timing-safe compare even when the supplied signature length is wrong, closing a small timing side-channel. (#55663) Thanks @gavyngong. ## 2026.3.28-beta.1 diff --git a/extensions/line/src/signature.test.ts b/extensions/line/src/signature.test.ts new file mode 100644 index 00000000000..7fefeaae9f1 --- /dev/null +++ b/extensions/line/src/signature.test.ts @@ -0,0 +1,34 @@ +import crypto from "node:crypto"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { validateLineSignature } from "./signature.js"; + +function sign(body: string, secret: string): string { + return crypto.createHmac("SHA256", secret).update(body).digest("base64"); +} + +describe("validateLineSignature", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("accepts a valid signature", () => { + const body = JSON.stringify({ events: [{ type: "message" }] }); + const secret = "top-secret"; + + expect(validateLineSignature(body, sign(body, secret), secret)).toBe(true); + }); + + it("still performs timing-safe comparison when signature length mismatches", () => { + const body = JSON.stringify({ events: [{ type: "message" }] }); + const secret = "top-secret"; + const spy = vi.spyOn(crypto, "timingSafeEqual"); + + expect(validateLineSignature(body, "short", secret)).toBe(false); + expect(spy).toHaveBeenCalledTimes(1); + + const [left, right] = spy.mock.calls[0] ?? []; + expect(left).toBeInstanceOf(Buffer); + expect(right).toBeInstanceOf(Buffer); + expect(left?.byteLength).toBe(right?.byteLength); + }); +});