PCI-DSS 4.0 Req 3.2 and 3.4 prohibit storing raw cardholder data outside an isolated, documented Cardholder Data Environment (CDE). A general-purpose database containing card numbers or CVVs is fully in scope for a Level 1 PCI audit — expanding your compliance surface and dramatically raising breach risk. Under OWASP A02 (Cryptographic Failures), exposed card numbers in an unprotected schema are trivially dumped via SQL injection or misconfigured backups. CWE-312 (Cleartext Storage) applies when tokenization is skipped. Req 12.4 requires that scope documentation exists; without it you cannot demonstrate compliance at all.
Critical because raw cardholder data in an undocumented or non-isolated environment is a direct PCI audit failure and exposes card numbers to mass exfiltration with no compensating control.
Delegate all card handling to a PCI-Level 1 processor (Stripe, Square) and verify your application never receives or stores raw PANs. If you must retain partial data (last-4, expiry month/year — never CVV per Req 3.3), isolate it in a separate database with Row-Level Security enabled and document the CDE boundary in docs/cde-architecture.md. Reference that file in your SAQ.
-- Separate CDE schema, row-level isolation
ALTER TABLE cardholder_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY cde_merchant_isolation ON cardholder_data
FOR ALL USING (merchant_id = auth.uid());
ID: ecommerce-pci.cardholder-data.cde-isolated
Severity: critical
What to look for: Enumerate all database tables, schemas, and API routes that reference cardholder data fields (card numbers, expiry dates, CVV, cardholder names tied to card data). Count every location where card data could be stored. Check whether cardholder data is explicitly stored in your system or delegated to a payment processor. Before evaluating, quote the actual database schema columns or API route field names you found.
Pass criteria: Cardholder data is NOT stored in your application database and primary system — at least 0 tables contain raw card number columns. It is either (1) handled exclusively by a PCI-compliant payment processor (e.g., Stripe, Square) with at least 1 documented integration point, or (2) stored in a clearly isolated, documented cardholder data environment with restricted access. Architecture documentation with at least 15 words explaining the CDE boundary exists in the project. Report even on pass: state the count of cardholder data references found (e.g., "0 raw card columns, 3 token references").
Fail criteria: Cardholder data is stored in general application databases or infrastructure without clear isolation, or no documentation exists showing how CDE is isolated. Do NOT pass when only token references exist but no CDE architecture documentation is present.
Skip (N/A) when: The project does not accept cardholder data (no payment provider dependency in package.json, no payment API routes, uses PayPal-only with no card handling).
Detail on fail: Specify where cardholder data was found or what's missing. Example: "Card numbers detected in production database schema (payments table has card_number column). No CDE isolation documented." or "CDE exists but isolation architecture not documented in any compliance file"
Cross-reference: See ecommerce-pci.cardholder-data.database-encryption (encryption at rest), ecommerce-pci.network-security.network-segmentation (network isolation), ecommerce-pci.access-control.pci-documentation (compliance docs).
Remediation: Never store cardholder data in your main application unless absolutely required and documented. The best practice is to delegate all card handling to a PCI-Level 1 processor like Stripe. Your application receives a token (e.g., Stripe payment method ID) and uses that token for transactions. If you must store some data (e.g., last-4 digits, expiry month/year without CVV), keep it in a separate, encrypted, access-restricted database:
-- Separate CDE database, encrypted columns
CREATE TABLE cardholder_data (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
last_four VARCHAR(4) NOT NULL,
expiry_month INT NOT NULL,
expiry_year INT NOT NULL,
card_brand VARCHAR(20) NOT NULL,
-- CVV is NEVER stored, not even encrypted
created_at TIMESTAMP DEFAULT NOW(),
encrypted_full_card BYTEA -- if required, use deterministic encryption
);
-- Store in separate, restricted database with RLS
ALTER TABLE cardholder_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY cde_merchant_isolation ON cardholder_data
FOR ALL USING (merchant_id = auth.uid());
Document your CDE architecture in a compliance file (e.g., docs/cde-architecture.md).