Skip to content
23 changes: 22 additions & 1 deletion lib/utils/razorpay-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,32 @@ function validateWebhookSignature (body, signature, secret) {

body = body.toString();

if (typeof secret !== 'string') {
secret = String(secret);
}

var expectedSignature = crypto.createHmac('sha256', secret)
.update(body)
.digest('hex');

return expectedSignature === signature;
if (typeof signature !== 'string') {
signature = String(signature);
}

if (signature.length !== expectedSignature.length || !/^[0-9a-f]+$/.test(signature)) {
return false;
}

try {
const a = Buffer.from(expectedSignature, 'utf8');
const b = Buffer.from(signature, 'utf8');
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
} catch (err) {
return false;
}
}

function validatePaymentVerification(params={}, signature, secret){
Expand Down
28 changes: 28 additions & 0 deletions test/utils/razorpay-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,34 @@ describe('Razorpay Utils', () => {
);
});

it('validateWebhookSignature uses timing-safe comparison', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two it(...) blocks are inside the callback of the existing it('validateWebhookSignature', ...), because that test's closing }); was moved down to line 136. Mocha doesn't run an it registered from inside a running test: it gets added to the root suite after that suite has already run, and nothing reports it. I checked this with mocha 9: an outer test containing two failing inner its passes, and the inner tests never show up in the spec reporter. So the 379 passing tests don't include either of the new ones.

Fix: close the existing test after its assert.ok(...) (line 107), then remove the extra }); at line 136. Also, the first new test is the same as the existing one with a different wrong signature, so it doesn't check that a constant-time comparison is used. You could drop it, or spy on crypto.timingSafeEqual and assert that it gets called.

const respBody = '{"a":1,"b":2,"c":{"d":3}}'
const secret = "123456"
const correctSignature = "2fe04e22977002e6c7cb553adab8b460cb" +
"9e2a4970d5953cb27a8472752e3bbc"
const wrongSignature = "000000000000000000000000000000000" +
"0000000000000000000000000000"

assert.ok(
validateWebhookSignature(respBody, correctSignature, secret) &&
!validateWebhookSignature(respBody, wrongSignature, secret),
'Timing-safe: validates webhook signature with correct and wrong signatures'
);
});

it('validateWebhookSignature rejects signatures of different length', () => {
const respBody = '{"test":"data"}'
const secret = "mysecret"
const shortSignature = "abcdef"
const longSignature = "a".repeat(128)

assert.ok(
!validateWebhookSignature(respBody, shortSignature, secret) &&
!validateWebhookSignature(respBody, longSignature, secret),
'Rejects signatures with different lengths than expected HMAC-SHA256 (64 hex chars)'
);
});

it('Subscription Verfication', () => {

const respBody = {
Expand Down