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.
Low because double-charge risk requires a specific failure mode (network timeout + retry) but the consequence — a customer charged twice — demands immediate manual 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.
ID: booking-flow-lifecycle.payment.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.json dependencies).
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.ts or your checkout handler).
const paymentIntent = await stripe.paymentIntents.create(
{
amount,
currency: 'usd',
metadata: { bookingId: booking.id }
},
{
idempotencyKey: booking.id // Prevent duplicate charges on retry
}
);