Advertisement

Developer how-tos

Test data and PCI DSS: keeping real card data out of non-production

A single real card number in a staging database, a log file or a test fixture can pull that whole system into PCI DSS scope. The fix is not more paperwork; it is test data that never contained real cards in the first place. This guide explains what PCI DSS v4 requires and how engineering teams meet it in practice.

By the Fakecreditcard teamPublished 8 min read

Advertisement

Key takeaways

  • PCI DSS v4 Requirement 6.5.5: live PANs are not used in pre-production environments, unless those environments are inside the CDE and fully protected.
  • Requirement 6.5.6: test data and test accounts are removed from system components before the system goes into production.
  • Use three kinds of synthetic data: generated Luhn-valid numbers for validation logic, gateway test cards for sandbox payments, and gateway test tokens (such as Stripe’s pm_card_visa) for server-side tests.
  • Never copy production card data to staging. Tokenize or replace it, mask logs to the last four digits, and scan repositories and logs for PANs.
On this page
  1. What PCI DSS v4 requires for test data
  2. Card data you must never keep anywhere
  3. Three kinds of synthetic card data
  4. Seeding staging and demo databases
  5. Copying production to staging safely
  6. Keeping card numbers out of logs
  7. Scanning repositories and logs for PANs
  8. A pre-production checklist
  9. Frequently asked questions

Payment teams usually guard production well. The leaks happen elsewhere: a support engineer pastes a customer’s card into a bug ticket, a debug log prints a request body, a “realistic” staging database is restored from last night’s backup. Each of those copies is cardholder data outside the protected environment. This guide covers the rules and the engineering habits that keep test systems clean.

What PCI DSS v4 requires for test data

PCI DSS v4.0 (and the v4.0.1 update) groups software change requirements under 6.5, “Changes to all system components are managed securely.” Two of them are about test data:

RequirementTextEarlier version
6.5.5“Live PANs are not used in pre-production environments, except where those environments are included in the CDE and protected in accordance with all applicable PCI DSS requirements.”v3.2.1 6.4.3 (production data not used for testing or development)
6.5.6“Test data and test accounts are removed from system components before the system goes into production.”v3.2.1 6.4.4

The exception in 6.5.5 is real but expensive: a staging environment that holds live PANs is part of the cardholder data environment (CDE), with the same access control, logging, encryption, segmentation and testing obligations as production. Very few teams want that. The practical reading is simple: pre-production gets synthetic data only.

Requirement 6.5.6 works in the other direction. Test accounts, default users, seeded customers and test card records must not ship into production, where they become backdoors or confuse reconciliation.

Card data you must never keep anywhere

Some data is off-limits even in production. Sensitive authentication data (SAD) — full track data, the card verification code and PINs — must not be retained after authorization, even encrypted (Requirement 3.3.1 and its sub-requirements). PCI SSC FAQ 1280 is explicit that card verification codes can’t be stored for card-on-file or recurring payments either. If a test needs a CVV, it should come from a fixture file of synthetic values, never from storage.

For the PAN itself, Requirement 3.4.1 limits what may be displayed to the BIN and the last four digits, and Requirement 3.5.1 requires stored PANs to be unreadable through truncation, index tokens, strong encryption or keyed hashes. These rules matter in non-production too: a masked value copied to staging is fine, a full PAN is not.

Data elementProductionPre-production
Full PANOnly if protected (truncated, tokenized, encrypted or keyed hash)Never live PANs; synthetic numbers only
BIN + last 4Allowed for displayFine as masked reference data
Cardholder name, expiryProtected when stored with the PANSynthetic values
CVV / CVC / CIDNever after authorizationNever stored; type test values into forms
Gateway tokens / customer IDsAllowedUse sandbox tokens only — production tokens don’t work in sandboxes
Advertisement

Three kinds of synthetic card data

Each layer of a payment integration needs a different kind of test data. Mixing them up is the most common reason teams reach for real cards.

1. Generated Luhn-valid numbers

For everything that happens before a request leaves your code — input masks, brand detection, length rules, Luhn validation, database constraints, import scripts — use generated numbers. They have the right prefix, length and check digit and are connected to no account. Our card generator produces them in bulk as CSV or JSON, and accepts a BIN or pattern when you need a specific range.

2. Gateway test cards

Payment sandboxes don’t accept arbitrary numbers. Each gateway documents its own test cards, and each card triggers a defined outcome: 4242 4242 4242 4242 succeeds in Stripe test mode, 4000 0000 0000 0002 is declined, and other numbers simulate insufficient funds or 3-D Secure. Our test card hub collects them for eight gateways, with links to each official source.

3. Gateway test tokens

Server-side tests should not send raw card numbers at all. Stripe’s testing documentation recommends test PaymentMethods such as pm_card_visa instead of card numbers, and warns that using card numbers directly in API calls or server-side code — even in tests — can leave that code out of PCI compliance when it goes live. Other gateways offer equivalent nonces or tokens. Tokens also make tests faster and deterministic.

Shell
# Stripe test mode: pay with a test PaymentMethod instead of a raw card number
curl https://api.stripe.com/v1/payment_intents \
  -u "$STRIPE_TEST_SECRET_KEY:" \
  -d amount=500 \
  -d currency=usd \
  -d payment_method=pm_card_visa \
  -d confirm=true \
  -d "automatic_payment_methods[enabled]=true" \
  -d "automatic_payment_methods[allow_redirects]=never"

Seeding staging and demo databases

Staging needs data that looks real enough to exercise reports, search and support tools. Build it from scratch with a seed script rather than from production:

  • Store what production stores. If production keeps a gateway token, last four digits, brand and expiry, the seed does the same — with sandbox tokens.
  • Use one recognizable test range for any full numbers (for example, documented gateway test cards), so scanners can allowlist them.
  • Generate names, emails and addresses with an obviously fake domain such as example.com, which is reserved for documentation.
  • Make the seed reproducible (fixed random seed) so bugs can be replayed.
Python
import random

TEST_CARDS = [  # documented Stripe test cards: (brand, number, sandbox PaymentMethod)
    ('visa', '4242424242424242', 'pm_card_visa'),
    ('mastercard', '5555555555554444', 'pm_card_mastercard'),
    ('amex', '378282246310005', 'pm_card_amex'),
]

def seed_customers(n: int, seed: int = 42):
    rnd = random.Random(seed)
    for i in range(n):
        brand, number, token = rnd.choice(TEST_CARDS)
        yield {
            'email': f'customer{i}@example.com',
            'card_brand': brand,
            'card_last4': number[-4:],          # what production stores
            'card_exp': f'{rnd.randint(1, 12):02d}/{rnd.randint(2028, 2032)}',
            'gateway_payment_method': token,   # sandbox token, never a production one
        }

Copying production to staging safely

Sometimes you do need production-shaped data — for a performance test or a migration rehearsal. The safe pattern is to transform the data inside the protected environment and export only the result:

  1. Run the masking job in production (or in a CDE-scoped copy), never on a staging server.
  2. Drop or replace every column that can hold a PAN, track data or CVV. Replace PANs with synthetic numbers of the same brand and length, or keep only the last four digits.
  3. Replace gateway tokens with sandbox tokens; production tokens are useless in a sandbox anyway.
  4. Scan the exported dump for PANs before it leaves (see the scanner below), and fail the job on any hit.
  5. Check free-text fields — notes, support messages, address lines, JSON blobs — where customers and agents paste card numbers.

Backups are copies too

Restoring a production backup into staging copies every PAN it contains. If you need to test restores, do it inside the CDE, or restore a masked export.

Keeping card numbers out of logs

Logs are the most common place for accidental PANs: request bodies, exception messages that include form data, and analytics events. Defend in layers: never log raw request bodies on payment endpoints, use gateway-hosted fields or SDKs so card data doesn’t touch your servers, and add a redaction filter as a last line of defense.

Python
import logging, re

PAN_RE = re.compile(r'\b(?:\d[ -]?){12,18}\d\b')

def luhn_ok(digits: str) -> bool:
    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2:
            d = d * 2 - 9 if d > 4 else d * 2
        total += d
    return total % 10 == 0

def redact(text: str) -> str:
    def mask(m):
        digits = re.sub(r'\D', '', m.group())
        return '[PAN ****' + digits[-4:] + ']' if luhn_ok(digits) else m.group()
    return PAN_RE.sub(mask, text)

class RedactPANs(logging.Filter):
    def filter(self, record):
        record.msg = redact(record.getMessage())
        record.args = ()
        return True

logging.getLogger().addFilter(RedactPANs())

Scanning repositories and logs for PANs

Redaction prevents new leaks; scanning finds the old ones. A useful scanner combines three filters to control false positives, because order numbers, timestamps and IDs often look like card numbers:

  1. Pattern: 13–19 digits, optionally grouped by spaces or dashes.
  2. Luhn: about one in ten random digit strings passes, so this removes roughly 90% of noise.
  3. Prefix: keep only numbers that start with a known card range (see our credit card regex guide for per-brand patterns).

Then allowlist the documented test numbers your team uses, so CI only fails on unexpected hits. Run the scanner in pre-commit hooks, in CI on every pull request, and periodically over log archives and database exports.

Python
import re, sys

CANDIDATE = re.compile(r'(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)')
CARD_PREFIX = re.compile(r'^(?:4|5[1-5]|2[2-7]|3[47]|3[0689]|6)')
ALLOWLIST = {'4242424242424242', '4111111111111111', '5555555555554444', '378282246310005'}

def luhn_ok(digits):
    s = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2:
            d = d * 2 - 9 if d > 4 else d * 2
        s += d
    return s % 10 == 0

def scan(path):
    with open(path, errors='ignore') as fh:
        for lineno, line in enumerate(fh, 1):
            for m in CANDIDATE.finditer(line):
                digits = re.sub(r'\D', '', m.group())
                if CARD_PREFIX.match(digits) and luhn_ok(digits) and digits not in ALLOWLIST:
                    yield lineno, digits[:6] + '…' + digits[-4:]   # never print the full number

found = [(p, n, masked) for p in sys.argv[1:] for n, masked in scan(p)]
for p, n, masked in found:
    print(f'{p}:{n}: possible PAN {masked}')
sys.exit(1 if found else 0)

A pre-production checklist

CheckHow to verify
No live PANs in any non-production databaseRun the scanner over a schema-wide export; review free-text columns
Card entry uses gateway-hosted fields or SDKInspect network requests: card data should go to the gateway, not your API
Server-side tests use gateway test tokensGrep the test suite for raw card numbers outside an allowlist
Logs redact card numbersUnit-test the redaction filter with test PANs in several formats
No CVV stored anywhereSearch schemas and event payloads for cvv, cvc, cid, security_code
Admin screens show last 4 onlyReview templates and exports
Test accounts and data removed before release (6.5.6)Release checklist step; query production for seed emails and test tokens
Staging refresh uses masked exports onlyDocument the pipeline; block direct backup restores

None of this replaces your QSA’s or acquirer’s guidance on scope, but it removes the most common reasons a test environment ends up holding real card data. To understand what a card number contains and which parts may be shown, see 6-digit vs 8-digit BINs; for testing a full payment flow without real cards, see how to test a Stripe checkout end to end.

Frequently asked questions

Can we use real card numbers in a staging environment?

Only if that environment is treated as part of the cardholder data environment and meets every applicable PCI DSS requirement (Requirement 6.5.5). For almost every team, it is cheaper and safer to use synthetic numbers and gateway test cards instead.

Are generated card numbers cardholder data?

A randomly generated number that belongs to no account is not a real PAN. Still, treat any 13–19 digit number that passes Luhn with a known BIN carefully in shared systems, because a scanner can’t tell the difference. Using documented test numbers or a clearly labeled test range makes allowlisting easy.

Can we store the CVV for testing or to retry payments later?

No. Card verification codes are sensitive authentication data and must not be retained after authorization (Requirement 3.3.1.2), including for card-on-file or recurring payments, as PCI SSC FAQ 1280 confirms. Your tests should never need a stored CVV.

How many digits of a card number can we show in an admin screen?

No more than the BIN and the last four digits, unless a documented business need justifies more (Requirement 3.4.1). Most admin and support screens only need the last four.

What should we do if we find a real PAN in logs or a repository?

Treat it as an incident: remove it from the source, purge backups and log archives according to your retention process, rotate any exposed credentials, check who had access and follow your incident response plan. For a repository, rewriting history is required, not just a new commit.

Sources

  1. PCI DSS v4.0.1 Requirement 6.5.5 (requirement text, Cloudaware reference)
  2. PCI DSS v4.0.1 Requirement 6.5.6 (requirement text, Cloudaware reference)
  3. PCI SSC FAQ 1492 — masking and truncation (Requirements 3.4.1, 3.5.1)
  4. PCI SSC FAQ 1280 — card verification codes for card-on-file or recurring payments
  5. Stripe Docs — Testing (test cards and PaymentMethods)

Tools used in this guide

Related guides

Written for developers, QA teams and merchants. Generated card numbers are test data: they are not real cards and cannot pay for anything.

Advertisement