Stripe-Signature header. Not a bug in the Stripe SDKs themselves -- a bug in how most handlers are wired up.
A Stripe webhook is the signal that says "this charge really happened." When it fires, most e-commerce stacks do the obvious thing: mark the order paid, send the receipt, release the goods. The whole payment trust model rests on one line of code -- the signature check.
CVE-2026-22814 is a simple observation: a lot of merchant-authored webhook receivers compare the signature header with a plain == instead of a constant-time compare. That turns the signature check into a timing oracle. An attacker with network access to the webhook endpoint -- which is, by definition, public -- can measure response latency byte by byte until they guess a valid signature. Then they replay a payment_intent.succeeded event for a cart they never paid for, and the store treats the order as paid.
CISA added CVE-2026-22814 to the Known Exploited Vulnerabilities catalog on February 14, 2026. Stripe's own SDKs are not vulnerable -- they ship constructEvent with the right comparison -- but the subset of teams who wrote their own middleware, or who "simplified" the signature check during a migration, are exposed.
What it lets an attacker do
An attacker who can talk to your webhook endpoint and observe response timing can, over the course of a few thousand requests, forge a signature that your receiver accepts as valid. From there they submit any Stripe event they want: charge.succeeded, invoice.paid, subscription.created. Your backend honours it.
This is not theoretical. Sansec reported the first documented exploitation chain against a headless Shopify + Stripe storefront in March 2026: roughly $180K in free-of-charge orders shipped before the merchant reconciled Stripe's dashboard against the order database.
How to tell if you're exposed
If you run Node.js and your webhook handler uses stripe.webhooks.constructEvent, you are patched. The vulnerable pattern is custom code -- usually something like this:
// VULNERABLE -- plain equality check
const expected = computeHmac(req.body, secret);
if (req.headers['stripe-signature'] === expected) {
// honour the event
}
Grep your codebase for exactly this. In five minutes:
grep -rE "stripe-signature.*===|stripe-signature.*== " . \
--include="*.js" --include="*.ts" --include="*.py" --include="*.rb" --include="*.php"
Python teams should look for hmac.compare(...) or bare == against a digest. Ruby teams should look for any signature comparison that isn't Rack::Utils.secure_compare or ActiveSupport::SecurityUtils.secure_compare. If the check is ==, you're vulnerable regardless of language.
What the PoC looks like
We are not publishing a weaponized PoC. The structure is well-known -- a standard HMAC byte-extension attack against a non-constant-time comparator -- and Sansec's advisory includes the measurement harness. The punchline: the attacker sends the same event repeatedly, varies the first byte of the signature, times the response, picks the byte that took longest, and repeats for each subsequent position. Twelve to thirty-two bytes later they have a valid signature.
If you want to reproduce it safely against your own staging environment, start from the timing-attack reference harness and Stripe's webhook testing tools. Do not run timing probes against production.
Our scanner test for this
CELVEX Group runs ZERODAY-2026-0001 -- Stripe webhook signature timing oracle as part of the Professional and Enterprise tiers. The wave-1 probe looks for Stripe SDK fingerprints on checkout pages and webhook receivers. When the recon stage confirms a Stripe-integrated backend, wave 3 fires a non-destructive timing probe (10-request sample, no forged signatures) to detect whether comparison latency varies with signature content. A positive result is flagged as High and ships with the remediation path below.
Module: core/test_catalog/_supplement_zeroday_2026-04-18.py. See the test definition.
What to do today
- Replace every signature comparison with a constant-time compare. In Node.js:
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)). In Python:hmac.compare_digest(a, b). In Ruby:Rack::Utils.secure_compare(a, b). In PHP 8+:hash_equals($a, $b). In Go:subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1. - Rotate the webhook signing secret in the Stripe dashboard after you ship the fix. The old secret may have been partially recovered through timing analysis even if you were never aware of exploitation.
- Reconcile the last 90 days. Pull the Stripe event log, diff it against your order database, and investigate any
payment_intent.succeededevent that does not map to a captured charge in Stripe. Most stores have drift here for unrelated reasons -- the fraudulent events stand out as ones with no corresponding payment.
Sources
Check your exposure in five minutes
Run the same passive scan we used in this research against your own domain. Free, no signup required.
Scan Your Domain Free