# No credit card data stored in application database

- **Pattern:** `ab-002264` (`saas-billing.payment-security.no-cc-data-stored`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002264
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002264)

## Why it matters

Storing raw card numbers, CVV, or full PANs in your database is a direct violation of PCI DSS Req 3.3.1 and Req 3.5.1 (pci-dss:4.0), classified under OWASP A02 (Cryptographic Failures) and CWE-312 (Cleartext Storage of Sensitive Information). A single database breach exposes every stored card to attackers. Payment providers deliberately design their systems so your application never needs to hold this data — storing it anyway buys you nothing except unlimited liability.

## Severity rationale

Critical because any stored raw card data is immediately exploitable in a breach and constitutes a per-se PCI DSS violation regardless of encryption.

## Remediation

Remove all raw card columns from your schema and replace them with provider-issued opaque identifiers. If you need to display card details to users (last 4 digits, brand), fetch that information from the provider's API at render time.

```ts
// prisma/schema.prisma — replace card_number / cvv columns with:
model User {
  stripe_customer_id           String? @unique
  stripe_default_payment_method String?
  // card_number String  <-- delete this
  // cvv         String  <-- delete this
}
```

To show "Visa ending in 4242" on the billing page, call `stripe.paymentMethods.retrieve(pm.id)` and read `pm.card.last4` and `pm.card.brand` — never cache those values to your own database.

## Detection

- **ID:** `no-cc-data-stored`
- **Severity:** `critical`
- **What to look for:** Search database schema files and migration files for fields that could contain raw card data. Look for column names like `card_number`, `card_num`, `cc_number`, `cvv`, `cvc`, `card_cvv`, `expiry`, `expiration_date`, `pan`, `full_card`. Also examine model definitions and any ORM query code that might write raw card strings to the database. Check API route handlers that process payment-related POST requests for any `db.insert` or similar calls that include card-like data. Examine any logging code near payment flows for accidental card data capture.
- **Pass criteria:** Enumerate all database schema columns related to payments — at least 0 PCI-sensitive columns expected. 0 columns contain raw card data. The schema may legitimately store provider-side tokens (`stripe_customer_id`, `stripe_payment_method_id`, `stripe_subscription_id`) — these are fine. Only raw card numbers, CVV, or magnetic stripe data fail this check.
- **Fail criteria:** Database schema or migration files contain columns for raw card data, OR API route code stores raw card-like strings to the database.
- **Skip (N/A) when:** No database detected (no ORM or database dependency in `package.json`, no schema files).
- **Detail on fail:** `"prisma/schema.prisma contains a 'card_number' field on the PaymentMethod model"` or `"API route stores raw card data to users table: found INSERT with 'card_number' field"`
- **Remediation:** Storing raw card data is a PCI DSS violation and creates catastrophic breach liability. Use your payment provider's customer and payment method objects instead:

  ```ts
  // Store only the provider's opaque identifiers
  await db.user.update({
    where: { id: userId },
    data: {
      stripe_customer_id: customer.id,       // OK
      stripe_default_payment_method: pm.id,  // OK
      // card_number: '4111...' — NEVER do this
    }
  })
  ```

  If you need to display partial card info to users (last 4 digits, card brand), retrieve it from the provider's API at display time rather than storing it yourself.

## External references

- pci-dss Req 3.3.1
- pci-dss Req 3.5.1
- cwe CWE-312
- owasp A02

Taxons: regulatory-conformance, data-integrity

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