Failed payment attempts are the primary signal for detecting carding attacks, credential stuffing, and account takeover attempts on your checkout. Without server-side audit logs, you cannot identify patterns — for example, 500 failed attempts against different card numbers from the same IP over one hour, or repeated failures on the same user account from different countries. PCI-DSS 4.0 Req 10.2 mandates logging of access control failures and user authentication attempts. CWE-778 (Insufficient Logging) and OWASP A09 (Security Logging and Monitoring Failures) both target exactly this gap. Client-side console.log calls are ephemeral and invisible to your backend — they don't count as audit logging.
Low because absent server-side payment failure logs eliminate the primary data source for detecting carding attacks, making fraud patterns invisible until they escalate to chargebacks.
Add structured server-side logging in every payment error catch block. Include enough context for pattern detection without logging PII or card data.
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
console.error(JSON.stringify({
event: 'payment_failed',
timestamp: new Date().toISOString(),
error_code: err.code,
decline_code: err.decline_code,
payment_intent_id: paymentIntentId,
amount_cents: amount,
currency,
user_id: session?.userId ?? 'anonymous',
// Do NOT log: card number, expiry, CVC, full email, full name
}))
return NextResponse.json({ error: 'Payment declined.' }, { status: 402 })
}
}
Also handle the payment_intent.payment_failed webhook event to capture declines that occur asynchronously. Ship these structured logs to your observability platform (Datadog, Grafana, CloudWatch) and set an alert when failure rate exceeds a threshold.
ID: ecommerce-payment-security.payment-errors.failed-payment-audit-log
Severity: low
What to look for: Count every catch block or error handler in payment API routes. For each, check whether a structured log entry is produced that includes at least 3 of: timestamp, error code, amount attempted, payment method type (not the card number), user/session identifier, IP address. Also check whether webhook events for payment_intent.payment_failed or charge disputes are handled and logged. Report the count: "X of Y payment error handlers include server-side audit logging."
Pass criteria: Failed payment attempts are logged server-side with sufficient context for security review: at least 3 data fields (timestamp, error code, session identifier at minimum). Report even on pass: "X payment error paths have structured logging with Y average fields per entry."
Fail criteria: Failed payment attempts are not logged at all, or are only logged to the browser console (which is client-side and ephemeral).
Skip (N/A) when: The project uses a fully hosted payment form where failed attempts are tracked entirely by the provider, and your backend receives no failed-attempt events.
Detail on fail: Describe what is missing. Example: "Payment failure catch blocks in app/api/payment/route.ts contain no logging — failed attempts produce no server-side record, making fraud pattern detection impossible"
Remediation: Add structured logging for all payment failures:
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Structured audit log — no PII, but enough for pattern detection
console.error(JSON.stringify({
event: 'payment_failed',
timestamp: new Date().toISOString(),
error_code: err.code,
decline_code: err.decline_code,
payment_intent_id: paymentIntentId,
amount_cents: amount,
currency,
user_id: session?.userId ?? 'anonymous',
// Do NOT log: card number, expiry, CVC, email, full name
}))
return NextResponse.json({ error: 'Payment declined.' }, { status: 402 })
}
}
Consider also handling payment_intent.payment_failed in your Stripe webhook to capture declines that happen asynchronously.