RevClamp Enterprise B2B REVENUE ASSURANCE Start Free
REVCLAMP ENGINEERING DISPATCH
← Back to Engineering Blog • WEBHOOK SECURITY & HMAC-SHA256 • 4 min read

Why JSON.stringify(req.body) breaks Stripe & Shopify webhook HMAC-SHA256 verification

When a Stripe 2025-03-31.basil subscription webhook, a Shopify Plus B2B orders/updated webhook (with financial_status: "partially_paid"), or a signed revclamp.entitlement.revoke circuit-breaker webhook reaches your backend, the HMAC-SHA256 digest in the HTTP header was computed over the exact raw UTF-8 request bytes transmitted over the wire.

Yet the single most common webhook verification bug in Node.js, Next.js App Router, and Express middleware is letting a body-parser deserialize the request first and then calling JSON.stringify(req.body) before computing the HMAC digest. Even a single stripped whitespace byte, reordered object key, or normalized Unicode escape (\u0026 vs &) changes the SHA-256 digest completely and causes intermittent 400 Bad Signature drops in production.

RAW-BODY HMAC-SHA256 & 300-SECOND REPLAY VERIFICATION PIPELINE 1. RAW UTF-8 BYTES await req.text() / Buffer NO JSON.stringify(body) Preserve exact byte stream 2. CANONICAL SIGNING Stripe/RC: t + "." + raw Shopify: raw UTF-8 bytes Hex (v1=) vs Base64 digest 3. TIMING-SAFE EQUAL crypto.timingSafeEqual Equal byte length check Zero side-channel leak 4. 300S REPLAY GUARD |nowSec - t| <= 300s Event ID deduplication Blocks stale replay attacks

1. Why parsed JSON objects fail cryptographic signature checks

When Express (express.json()) or a Next.js route handler parses an incoming webhook body into a JavaScript object and re-serializes it via JSON.stringify(req.body), three byte-level mutations occur:

2. Provider-specific canonical signing differences: Stripe vs. Shopify vs. RevClamp

Even when you capture the exact raw UTF-8 buffer via await request.text(), each B2B commerce and billing provider uses a distinct signing envelope and digest encoding:

Failure Mode / Provider Rule Header & Canonical Input Naive Implementation Bug Deterministic Verification Guard
1. Parsed JSON Re-Serialization Raw UTF-8 request body buffer Computes HMAC over JSON.stringify(req.body) Read raw bytes first (await req.text()) before JSON.parse
2. Stripe Timestamped Hex Envelope Stripe-Signature: t=<unix>,v1=<hex> over {t}.{rawBody} Hashes rawBody without prepending {t}. or ignores multiple v1= keys Construct {timestampSec}.${rawPayload} and verify lower-case hex digest
3. Shopify Base64 Digest Encoding X-Shopify-Hmac-Sha256: <base64> over raw UTF-8 body Compares .digest("hex") against Shopify's Base64 header string Compute .digest("base64") over raw UTF-8 bytes with constant-time compare
4. Stale Webhook Replay Attack t=<unix_seconds> in Stripe-Signature / X-RevClamp-Signature Verifies HMAC match but omits timestamp skew tolerance check Reject with SIGNATURE_TIMESTAMP_OUTSIDE_TOLERANCE when |nowSec - t| > 300s
5. Timing Side-Channel & Secret Leak Signature comparison & browser diagnostic tools Uses === string comparison or pastes live whsec_ / shpss_ keys into web tools Use crypto.timingSafeEqual and enforce LIVE_SECRET_FORBIDDEN_IN_SANDBOX

3. Testing webhook signatures via MCP (revclamp_verify_webhook_signature) or Browser Sandbox

Both our interactive browser utility (/tools/webhook-signature-tester) and our 6th Model Context Protocol tool (revclamp_verify_webhook_signature on POST https://api.revclamp.com/api/mcp) run the exact same deterministic verifier with a Zero-Trust guard that blocks live credentials (sk_live_, rk_live_, non-test whsec_..., shpss_..., shpat_..., rg_live_, rc_live_, AKIA..., and PEM private keys):

{
  "jsonrpc": "2.0",
  "id": "webhook-hmac-check-1",
  "method": "tools/call",
  "params": {
    "name": "revclamp_verify_webhook_signature",
    "arguments": {
      "provider": "stripe",
      "secret": "whsec_test_secret_revclamp_sandbox_2026",
      "rawPayload": "{\"id\":\"evt_basil_sandbox_01\",\"api_version\":\"2025-03-31.basil\",\"type\":\"customer.subscription.updated\"}"
    }
  }
}
RevClamp Enterprise -- B2B Revenue & Compliance Guardrails
Open Free Webhook Signature Tester → Test Live in MCP Playground View Next.js 12-Line Edge Guard Start Free Workspace (5,000 events/mo)