Skip to content

feat(webhook_verify): timing-safe, replay-resistant webhook signature verification module (29 tests, zero deps) - #22

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
Larslllllll:feat/webhook-verify-module
Aug 19, 2026
Merged

feat(webhook_verify): timing-safe, replay-resistant webhook signature verification module (29 tests, zero deps)#22
ralyodio merged 1 commit into
profullstack:masterfrom
Larslllllll:feat/webhook-verify-module

Conversation

@Larslllllll

Copy link
Copy Markdown
Contributor

Refs #2 — you asked for more community packages, so here is one that fills a gap I did not see covered by the existing modules.

What it does

webhook_verify verifies inbound webhook signatures for Stripe, GitHub, Slack, Shopify, Twilio and generic hex HMAC. Zero dependencies — node:crypto only.

Why this one

Webhook verification is the security code almost every backend rewrites, and it fails in four predictable ways:

Mistake Consequence
signature === expected Byte-by-byte comparison leaks the position of the first wrong byte through timing
No timestamp check A captured valid request stays valid forever — replay it tomorrow and it passes
Verifying the parsed body Re-serialising shifts key order or whitespace, the HMAC stops matching, and people "fix" it by disabling verification
Returning bare false A caller writes if (verify(...)) and a truthy value slips through

The module inverts each one:

  • timingSafeEqual everywhere, with an explicit length check firsttimingSafeEqual throws on unequal lengths, and length is itself a leak
  • replay windows on every provider that signs a timestamp, symmetric so a future-dated timestamp is rejected too
  • verification against the raw body; nothing is decoded before the signature is checked
  • a structured result with a specific reason (signature_mismatch, timestamp_out_of_range, malformed_signature, unsupported_provider) plus advice: "Do not parse or act on this payload."

Stripe secret rotation is handled: a header carrying several v1= values passes if any one matches.

Layout

Follows mcp_modules/template exactly:

mcp_modules/webhook_verify/
  index.js                 register() + /tools/webhook_verify + metadata
  src/service.js           verification logic
  src/controller.js        HTTP handlers
  src/utils.js             header extraction (case-insensitive) + secret redaction
  test/service.test.js     23 tests
  test/utils.test.js       6 tests
  examples/basic-usage.js  runnable
  docs/api.md
  README.md
  package.json

Tests

29 passing (mocha + chai):

stripe        valid · tampered body · replayed · future-dated · multi-v1 rotation · no timestamp · non-numeric timestamp
github        valid · wrong secret · missing sha256= prefix · truncated signature (no throw)
slack         valid · stale request
shopify       valid · non-base64
twilio        valid · params changed
hmac          valid · non-hex
dispatcher    unsupported provider · missing secret · advice present on failure · never a bare boolean
utils         case-insensitive headers · slack timestamp · unknown provider · redaction (3)

A failed verification returns HTTP 200 with valid: false — it is a valid answer, not a server error. Missing or unsupported parameters return 400.

Happy to rename the module, adjust the endpoint prefix, or split providers if you would rather they were separate modules.

Adds a community module for the one piece of security code most backends rewrite and get
wrong. Supports Stripe (incl. multi-v1 secret rotation), GitHub, Slack, Shopify, Twilio and
generic hex HMAC.

Design rules, each covered by a test:
- every digest comparison goes through timingSafeEqual, with an explicit length check first
- replay windows on every provider that signs a timestamp, symmetric so future-dated
  timestamps are rejected too
- verification runs against the raw body; nothing is parsed before the signature is checked
- failures return a specific reason (signature_mismatch / timestamp_out_of_range /
  malformed_signature) instead of a bare false, so a falsy value cannot be mistaken for a
  handled failure

Zero dependencies beyond node:crypto. 29 mocha/chai tests. Follows the template module
layout: index.js register(), src/{service,controller,utils}.js, test/, examples/, docs/.

Refs profullstack#2
import { createHmac } from 'node:crypto';
import { verify } from '../src/service.js';

const SECRET = 'whsec_example';
@ralyodio
ralyodio merged commit ecb0ad3 into profullstack:master Aug 19, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants