Retry mechanism is safe
Why it matters
Network timeouts during payment API calls are a normal operational event, and every retry without an idempotency key is a potential double charge. CWE-605 (multiple binds to the same port) and CWE-841 both describe re-entrancy hazards; in payment systems the re-entrancy produces a duplicate charge that Stripe will honor because each request without an idempotency key is treated as a new, independent charge. A customer who retries a failed payment page reload can be charged multiple times. Idempotency keys that are random UUIDs regenerated on each retry defeat the purpose — the key must be deterministically derived from the booking ID so retries produce the same result.
Severity rationale
Low because double-charge risk requires a specific failure mode (network timeout + retry) but the consequence — a customer charged twice — demands immediate manual remediation.
Remediation
Pass a deterministic idempotency key tied to the booking ID in every payment API call. Implement this in src/lib/payment-service.ts or your checkout handler.
const paymentIntent = await stripe.paymentIntents.create(
{
amount,
currency: 'usd',
metadata: { bookingId: booking.id },
},
{
idempotencyKey: `booking-${booking.id}`, // deterministic — same key on retry
}
);
Also add a processed-event check in your webhook handler to guard against Stripe replaying events.
Detection
-
ID:
retry-mechanism -
Label: Retry mechanism is safe
-
Severity:
low -
What to look for: Count all payment API calls (create payment intent, create charge, create checkout session) and verify each one includes an idempotency key. Search for
idempotencyKey,Idempotency-Key,request_id, or equivalent. Also check webhook handlers for idempotent processing (e.g., checking if the event was already processed before acting on it). Enumerate: "X of Y payment API calls include idempotency keys." -
Pass criteria: At least 1 payment API call includes a unique idempotency key (usually the booking ID or a deterministic hash of booking ID + amount). Webhook handlers must also be idempotent (check for duplicate event processing). Report:
"X of Y payment API calls use idempotency keys. Webhook idempotency: [yes/no]." -
Fail criteria: Retries could cause double charges. No idempotency keys used in any payment API call. Do NOT pass when the idempotency key is a random UUID generated on each retry — it must be deterministic (tied to the booking ID or transaction).
-
Skip (N/A) when: No payment integration (no Stripe, PayPal, Square, or other payment SDK in
package.jsondependencies). -
Detail on fail:
"No idempotency key used in Stripe payment API calls. A network timeout and retry could result in double charges." -
Cross-reference: The payment-booking-coupling check in this category provides the booking ID commonly used as the idempotency key.
-
Cross-reference: The payment-capture-timing check in this category verifies the payment flow where retries could occur.
-
Cross-reference: The failure-handling check in this category verifies what happens when a retried payment ultimately fails.
-
Remediation: Pass idempotency keys in all payment API calls (e.g., in
src/lib/payment-service.tsor your checkout handler).const paymentIntent = await stripe.paymentIntents.create( { amount, currency: 'usd', metadata: { bookingId: booking.id } }, { idempotencyKey: booking.id // Prevent duplicate charges on retry } );
External references
- cwe · CWE-605 — Multiple Binds to the Same Port
- cwe · CWE-841 — Improper Enforcement of Behavioral Workflow
- iso-25010:2011 · reliability.fault-tolerance — Fault Tolerance — deterministic idempotency keys prevent double-charge on payment retry
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-flow-lifecycle·automated