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.
Critical because any stored raw card data is immediately exploitable in a breach and constitutes a per-se PCI DSS violation regardless of encryption.
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.
// 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.
ID: saas-billing.payment-security.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:
// 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.