From b57c361b976148da8ac7c682a34018985486adb4 Mon Sep 17 00:00:00 2001 From: yanivnimni Date: Mon, 4 Aug 2025 16:28:41 +0300 Subject: [PATCH 1/3] updated webhook verify to be more secure --- resources/webhooks.ts | 4 +++- tests/webhooks.spec.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/resources/webhooks.ts b/resources/webhooks.ts index 29c96d4c..fd30c9c8 100644 --- a/resources/webhooks.ts +++ b/resources/webhooks.ts @@ -42,9 +42,11 @@ export class Webhooks extends BaseResource { } public verify(signature: string, secret: string, payload: any) { + const signatureBuffer = Buffer.from(signature, "base64") const hmac = crypto.createHmac("sha1", secret) hmac.update(JSON.stringify(payload)) - return hmac.digest("base64") == signature + + return crypto.timingSafeEqual(hmac.digest(), signatureBuffer) } } diff --git a/tests/webhooks.spec.ts b/tests/webhooks.spec.ts index 5abb3f1c..3091b5b6 100644 --- a/tests/webhooks.spec.ts +++ b/tests/webhooks.spec.ts @@ -22,4 +22,15 @@ describe("Get Webhook Test", () => { expect(res.data.type === "webhook").toBeTruthy() }) }) +}) + +describe("Verify Webhook test", () => { + test("verify webhook signature", () => { + const signature = "TzmTqUPhGiyHfKcpYoXePi/EVf0=" + const secret = "QK89mgP2v9KPGXVRp92IfYtHpbzrLpsjMp6sfWOPasQ=" + const payload = {"data":[{"id":"24","type":"application.created","attributes":{"createdAt":"2025-08-04T13:12:33.887Z","tags":{"key":"another-tag","test":"webhook-tag","number":"111"}},"relationships":{"application":{"data":{"id":"10006","type":"individualApplication"}}}}],"included":[{"id":"10006","type":"individualApplication","attributes":{"ssn":"663885441","tags":{"key":"another-tag","test":"webhook-tag","number":"111"},"email":"cheryl.mercado@mymail.com","phone":{"number":"3476042441","countryCode":"1"},"status":"New","address":{"city":"Cedar Falls","state":"IA","street":"26 Cardinal Dr.","country":"US","postalCode":"50613"},"message":"Pre created application","archived":false,"fullName":{"last":"Mercado","first":"Cheryl"},"createdAt":"2025-08-04T13:12:33.887Z","maskedSSN":"*****5441","occupation":"Doctor","dateOfBirth":"1946-04-11","evaluationId":null,"decisionMethod":null,"decisionReason":null,"decisionUserId":null,"evaluationCodes":null,"evaluationScores":null,"evaluationOutcome":null,"evaluationEntityId":null,"soleProprietorship":false},"relationships":{"org":{"data":{"id":"2","type":"org"}}}}]} + + const verifyResult = unit.webhooks.verify(signature, secret, payload) + expect(verifyResult).toBeTruthy() + }) }) \ No newline at end of file From 6917da00ced91e0c7253dd739bd4586e0f8b82bf Mon Sep 17 00:00:00 2001 From: yanivnimni Date: Mon, 4 Aug 2025 16:59:15 +0300 Subject: [PATCH 2/3] update error message --- types/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/common.ts b/types/common.ts index a7af55f9..7b05ca25 100644 --- a/types/common.ts +++ b/types/common.ts @@ -519,7 +519,7 @@ export interface UnitErrorPayload { export const extractUnitError = (underlying: any): UnitError => { // for now, we only support extracting a UnitError from an axios error if (!underlying || !axiosStatic.isAxiosError(underlying) || !underlying.response) { - return new UnitError("Unknown Error", underlying) + return new UnitError(`Unknown Error - ${underlying.message}`, underlying) } let message = `${underlying.response.status} - ${underlying.response.statusText ?? "Error"}` From 5bdbafa87112522a7e609f997689eccaf5aaedf0 Mon Sep 17 00:00:00 2001 From: yanivnimni Date: Tue, 5 Aug 2025 09:55:32 +0300 Subject: [PATCH 3/3] change test params --- tests/webhooks.spec.ts | 6 +++--- types/common.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/webhooks.spec.ts b/tests/webhooks.spec.ts index 3091b5b6..e7032203 100644 --- a/tests/webhooks.spec.ts +++ b/tests/webhooks.spec.ts @@ -26,9 +26,9 @@ describe("Get Webhook Test", () => { describe("Verify Webhook test", () => { test("verify webhook signature", () => { - const signature = "TzmTqUPhGiyHfKcpYoXePi/EVf0=" - const secret = "QK89mgP2v9KPGXVRp92IfYtHpbzrLpsjMp6sfWOPasQ=" - const payload = {"data":[{"id":"24","type":"application.created","attributes":{"createdAt":"2025-08-04T13:12:33.887Z","tags":{"key":"another-tag","test":"webhook-tag","number":"111"}},"relationships":{"application":{"data":{"id":"10006","type":"individualApplication"}}}}],"included":[{"id":"10006","type":"individualApplication","attributes":{"ssn":"663885441","tags":{"key":"another-tag","test":"webhook-tag","number":"111"},"email":"cheryl.mercado@mymail.com","phone":{"number":"3476042441","countryCode":"1"},"status":"New","address":{"city":"Cedar Falls","state":"IA","street":"26 Cardinal Dr.","country":"US","postalCode":"50613"},"message":"Pre created application","archived":false,"fullName":{"last":"Mercado","first":"Cheryl"},"createdAt":"2025-08-04T13:12:33.887Z","maskedSSN":"*****5441","occupation":"Doctor","dateOfBirth":"1946-04-11","evaluationId":null,"decisionMethod":null,"decisionReason":null,"decisionUserId":null,"evaluationCodes":null,"evaluationScores":null,"evaluationOutcome":null,"evaluationEntityId":null,"soleProprietorship":false},"relationships":{"org":{"data":{"id":"2","type":"org"}}}}]} + const signature = "UUNz8ch1Ovjg+ijXUEwlAlWEktU=" + const secret = "OB2HL5E3B4HJ7IVXRNL4YQKYIQIVJK36ZZLPZEFWZVSDSC7LLFJQ====" + const payload = {"data":[{"id":"46306092","type":"application.approved","attributes":{"createdAt":"2025-08-05T06:48:38.957Z","tags":{"key":"another-tag","test":"webhook-tag","number":"111"}},"relationships":{"application":{"data":{"id":"3895367","type":"individualApplication"}},"customer":{"data":{"id":"3310133","type":"individualCustomer"}}}}]} const verifyResult = unit.webhooks.verify(signature, secret, payload) expect(verifyResult).toBeTruthy() diff --git a/types/common.ts b/types/common.ts index 7b05ca25..a7af55f9 100644 --- a/types/common.ts +++ b/types/common.ts @@ -519,7 +519,7 @@ export interface UnitErrorPayload { export const extractUnitError = (underlying: any): UnitError => { // for now, we only support extracting a UnitError from an axios error if (!underlying || !axiosStatic.isAxiosError(underlying) || !underlying.response) { - return new UnitError(`Unknown Error - ${underlying.message}`, underlying) + return new UnitError("Unknown Error", underlying) } let message = `${underlying.response.status} - ${underlying.response.statusText ?? "Error"}`