October 31, 2026 · Muhammad Rehan · More by Muhammad Rehan
stripe-node Quietly Changed How Webhook Timestamp Checks Work
Two stripe-node releases changed webhook timestamp-tolerance defaults for verifyHeader() and constructEvent(). Here's what shifted, and what to check.

For webhook integrations, “we didn’t touch that code” is not the same as “that code still behaves the same way.” Two recent stripe-node releases prove it: the library’s timestamp-tolerance logic for webhook signature verification shipped with two bugs that broke in opposite directions, and both got fixed without a security advisory attached.
Two Fixes, Two Opposite Directions
The first fix landed in stripe-node 23.0.0, released September 30, 2026. stripe.webhooks.signature.verifyHeader() and verifyHeaderAsync() previously skipped timestamp-tolerance verification entirely whenever the tolerance argument was omitted. As of 23.0.0, omitting it now defaults to Webhook.DEFAULT_TOLERANCE — so code written against the old behavior, which treated “no tolerance argument” as “no expiry check,” now gets an expiry check enforced where none ran before.
The second fix shipped in the same 23.0.0 release, in the opposite direction. stripe.webhooks.constructEvent() previously treated a tolerance: 0 argument as if the caller had passed the default tolerance, silently ignoring the explicit intent to disable the check. Now, passing 0 “properly skips timestamp tolerance verification” — matching what Stripe’s documentation has always said a tolerance of 0 does.
Why Tolerance Exists in the First Place
Stripe’s webhook documentation frames the timestamp as replay-attack protection: the Stripe-Signature header carries a timestamp that’s part of the signed payload, so an attacker who captures a valid request can’t alter it without invalidating the signature, but could still resend it unchanged. “Our libraries have a default tolerance of 5 minutes between the timestamp and the current time,” the docs state, and they explicitly warn against the alternative: “Don’t use a tolerance value of 0. Using a tolerance value of 0 disables the recency check entirely.” That’s precisely the behavior constructEvent() failed to deliver before 23.0.0, and precisely the behavior verifyHeader() delivered by accident when no tolerance argument was passed at all.
A Related Fix in the Next Release
A related fix shipped in the next release, stripe-node 23.0.1, on October 22, 2026: the library now rejects webhook signing secrets made entirely of ASCII whitespace instead of using them as HMAC keys, closing off a misconfiguration that would previously have passed signature checks against an effectively empty key.
What to Check in Your Integration
None of this shows up in a diff of your own code, which is exactly why it’s worth a direct check. If your integration calls verifyHeader() or verifyHeaderAsync() without an explicit tolerance argument, you’re now getting the library’s default 5-minute expiry check where you previously got none — worth confirming nothing depended on that gap, such as reprocessing old payloads from a queue. If anything in your codebase passes tolerance: 0 expecting lenient behavior, it now does exactly what the docs always said it would: disable the recency check completely, which is a real reduction in replay protection if it wasn’t intentional.
These two fixes are confirmed for stripe-node specifically. Other official Stripe SDKs weren’t checked as part of this research, and shouldn’t be assumed to have changed the same way.