# Decline codes are logged internally but not exposed to the client

- **Pattern:** `ab-001170` (`ecommerce-payment-security.fraud-prevention.decline-codes-internal-only`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001170
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001170)

## Why it matters

Stripe's `decline_code` field on a `StripeCardError` carries values like `stolen_card`, `fraudulent`, `card_velocity_exceeded`, and `insufficient_funds`. If your API forwards this code to the browser, you are giving card-testing bots a real-time oracle: they can distinguish cards that are blocked as stolen from cards that are simply declined for insufficient funds, and continue probing with the latter. CWE-209 (Information Exposure Through Error Message) and CWE-200 (Exposure of Sensitive Information) both apply. OWASP A05 (Security Misconfiguration) covers this as information leakage. The internal `decline_code` is a legitimate signal for your fraud team — it should flow to your logging pipeline, not to the client.

## Severity rationale

Medium because exposing decline codes to clients gives card-testing bots a free oracle to distinguish stolen-and-blocked cards from valid-but-declined ones, improving attack efficiency.

## Remediation

Log the `decline_code` server-side for your fraud and support teams, and return a single generic message regardless of the specific decline reason.

```ts
} catch (err) {
  if (err instanceof Stripe.errors.StripeCardError) {
    // Internal: log the specific code for fraud pattern analysis
    console.error('Card declined', {
      decline_code: err.decline_code,  // 'stolen_card', 'insufficient_funds', etc.
      code: err.code,
      paymentIntentId,
    })

    // External: generic message regardless of decline reason
    return NextResponse.json(
      { error: 'Your payment was declined. Please contact your card issuer or try a different card.' },
      { status: 402 }
    )
  }
}
```

Do not map specific decline codes to user-facing messages like "Insufficient funds" — that narrows the attack surface for carding bots by confirming the card number is valid.

## Detection

- **ID:** `decline-codes-internal-only`
- **Severity:** `medium`
- **What to look for:** Check error handling code for payment API calls specifically around decline scenarios. For Stripe, a declined card results in a `StripeCardError` with a `decline_code` property (values like `insufficient_funds`, `card_velocity_exceeded`, `stolen_card`, `fraudulent`). Search for where `decline_code` is accessed and whether that value or an equivalent gets serialized into the API response body. Count all error response paths that handle card declines and for each, verify `decline_code` stays server-side only.
- **Pass criteria:** Decline codes are captured and logged server-side (e.g., written to application logs or a monitoring service). The client receives only a generic decline message without the specific code. Different decline types produce the same generic user-facing message. No more than 0 decline code values should appear in API response bodies.
- **Fail criteria:** The specific decline code (e.g., `stolen_card`, `fraudulent`, `insufficient_funds`) is included in the API response body and displayed to the user. This can help fraudsters understand which card numbers are flagged as stolen vs. just declined.
- **Skip (N/A) when:** Never — decline code handling applies to all projects that process card payments.
- **Detail on fail:** Describe what is exposed. Example: `"Error response JSON includes { declineCode: 'stolen_card' } — exposing this to the client helps card testers distinguish blocked cards from valid-but-declined ones"`
- **Remediation:** Log decline codes server-side and return only generic messages:

  ```ts
  } catch (err) {
    if (err instanceof Stripe.errors.StripeCardError) {
      // Log internally — never expose decline_code to client
      console.error('Card declined', {
        decline_code: err.decline_code, // 'insufficient_funds', 'stolen_card', etc.
        code: err.code,
        paymentIntentId,
      })

      // Generic message to client regardless of decline reason
      return NextResponse.json(
        { error: 'Your payment was declined. Please contact your card issuer or try a different card.' },
        { status: 402 }
      )
    }
  }
  ```

  Avoid mapping specific decline codes to user-facing messages like "insufficient funds" — this narrows the attack surface for card-testing bots.

---

## External references

- cwe CWE-209
- cwe CWE-200
- owasp A05

Taxons: injection-and-input-trust, observability

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