When a Stripe charge succeeds but the subsequent database write fails, you have taken the customer's money without creating their order record. Without compensation logic, this is a silent failure: your system shows no order, the customer sees no confirmation, and no automated process triggers a refund or retry. CWE-362 (Race Condition / Improper Synchronization) applies when the payment and fulfillment steps are not treated as an atomic operation. In high-volume stores, even a 0.1% database failure rate translates to dozens of unreconciled charges per day — each requiring manual support intervention, chargeback risk, and reputational damage.
High because an unhandled post-payment database failure charges the customer without creating their order, requiring manual intervention for every occurrence and creating chargeback exposure.
Wrap all post-payment database writes in a transaction, and provide explicit compensation (refund or retry queue) if the transaction fails.
// In your webhook handler after verifying signature
if (event.type === 'payment_intent.succeeded') {
const pi = event.data.object as Stripe.PaymentIntent
try {
await db.transaction(async (tx) => {
await tx.orders.create({ stripePaymentIntentId: pi.id, status: 'confirmed' })
await tx.inventory.decrement(/* items */)
})
} catch (err) {
console.error('Post-payment DB failure', { paymentIntentId: pi.id, err })
// Compensation: refund the charge so the customer is not stuck
await stripe.refunds.create({ payment_intent: pi.id })
// Or: push to a dead-letter queue for manual review + retry
await queue.push({ type: 'fulfill-order', paymentIntentId: pi.id })
}
}
Use Stripe's metadata field on the PaymentIntent to store the internal order ID, so any retry or support lookup can correlate the charge to your records.
ID: ecommerce-payment-security.client-side-handling.post-payment-rollback
Severity: high
What to look for: Trace the code that runs after a payment is confirmed. List all post-payment database write operations (order records, inventory updates, subscription records) and for each, classify the error handling as: (a) transaction-wrapped, (b) compensating refund logic, (c) retry/queue mechanism, or (d) no error handling. Count the total operations and those with adequate protection.
Pass criteria: Post-payment database operations are either: (a) wrapped in a transaction that rolls back on failure, (b) accompanied by compensation logic that issues a refund if the DB write fails, or (c) designed to be idempotent and retryable. At least 1 of these mechanisms must be present for every post-payment write path.
Fail criteria: Post-payment database writes are bare await db.create(...) calls with no error handling. If the database write fails after a successful charge, the customer is charged but receives no order, with no mechanism for recovery.
Skip (N/A) when: The project has no post-payment database writes — for example, a pure PaymentIntent-only flow with no order record creation.
Detail on fail: Describe the unprotected sequence. Example: "app/api/webhooks/stripe/route.ts: on payment_intent.succeeded, calls db.orders.create() with no try/catch and no refund logic — a DB failure would charge the customer without creating their order"
Remediation: Wrap post-payment operations in try/catch with compensation logic:
// In your webhook handler after verifying signature
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object as Stripe.PaymentIntent
try {
await db.transaction(async (tx) => {
await tx.orders.create({
stripePaymentIntentId: paymentIntent.id,
amount: paymentIntent.amount,
status: 'confirmed',
// ...
})
await tx.inventory.decrement(/* items */)
})
} catch (err) {
// Log the failure for manual review
console.error('Post-payment DB write failed', { paymentIntentId: paymentIntent.id, err })
// Option A: Issue a refund immediately
await stripe.refunds.create({ payment_intent: paymentIntent.id })
// Option B: Queue for retry with an idempotent worker
await queue.push({ type: 'fulfill-order', paymentIntentId: paymentIntent.id })
}
}