fix(webhooks): use timingSafeEqual for signature comparison - #43
fix(webhooks): use timingSafeEqual for signature comparison#43tsushanth wants to merge 1 commit into
Conversation
Replace the string equality check (signature !== expectedSignature) with crypto.timingSafeEqual so that the comparison runs in constant time. A naive !== comparison on hex strings leaks secret key material through timing differences: an attacker who can send many requests and measure response latency can recover the expected signature one character at a time (a classic timing side-channel). Node's crypto.timingSafeEqual compares two equal-length Buffers in O(n) constant time regardless of where they first differ. A preceding length check is safe because the expected signature is always exactly 64 hex characters (SHA-256), so differing lengths reveal nothing about the key.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 85faa10. Configure here.
| const signaturesMatch = crypto.timingSafeEqual( | ||
| Buffer.from(signature, "utf8"), | ||
| Buffer.from(expectedSignature, "utf8"), | ||
| ); |
There was a problem hiding this comment.
Unsafe timing-safe length check
Medium Severity
The length guard compares JavaScript string .length, but crypto.timingSafeEqual compares UTF-8 buffer byte lengths. A signature with the same character count as the expected hex digest can still encode to a different byte length (e.g. non-ASCII characters), causing timingSafeEqual to throw. Callers rely on a Result return and typically do not catch exceptions here, so verification can crash instead of returning an invalid-signature error.
Reviewed by Cursor Bugbot for commit 85faa10. Configure here.


What
Replace the plain string equality check in
verifyWebhookSignaturewithcrypto.timingSafeEqual.Before
After
Why
JavaScript string comparison with
!==short-circuits at the first differing character, so response latency leaks how many leading characters of the expected signature matched. An attacker who can send many requests and measure response time can brute-force the expected HMAC value one hex character at a time — effectively recovering the signing secret offline.crypto.timingSafeEqualcompares two equal-lengthBuffers in constant time regardless of content. This is the standard fix used by Express, Stripe's SDK, and the Node.js docs example for HMAC verification.The preceding length check is safe: the expected signature is always exactly 64 hex characters (SHA-256 digest), so a differing length reveals nothing about the key.
References
Note
Low Risk
Small, localized hardening in webhook verification with unchanged error behavior for callers.
Overview
Webhook signature verification in
verifyWebhookSignatureno longer uses!==on the hex HMAC strings. It now rejects signatures whose length does not match the fixed SHA-256 hex digest length, then compares the provided and expected values withcrypto.timingSafeEqualon UTF-8 buffers.Invalid signatures still return the same
PlainWebhookSignatureVerificationErrormessage; only the comparison path changes to avoid timing leaks from short-circuiting string equality.Reviewed by Cursor Bugbot for commit 85faa10. Bugbot is set up for automated code reviews on this repo. Configure here.