# Audit logging of failed payment attempts for security monitoring

- **Pattern:** `ab-001174` (`ecommerce-payment-security.payment-errors.failed-payment-audit-log`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001174
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001174)

## Why it matters

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.

## Severity rationale

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.

## Remediation

Add structured server-side logging in every payment error catch block. Include enough context for pattern detection without logging PII or card data.

```ts
} 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.

## Detection

- **ID:** `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:

  ```ts
  } 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.

## External references

- cwe CWE-778
- owasp A09
- pci-dss Req 10.2

Taxons: observability, access-control

HTML version: https://auditbuffet.com/patterns/ab-001174
