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.
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
- What PCI DSS v4 requires for test data
- Card data you must never keep anywhere
- Three kinds of synthetic card data
- Seeding staging and demo databases
- Copying production to staging safely
- Keeping card numbers out of logs
- Scanning repositories and logs for PANs
- A pre-production checklist
- 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:
| Requirement | Text | Earlier 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 element | Production | Pre-production |
|---|---|---|
| Full PAN | Only if protected (truncated, tokenized, encrypted or keyed hash) | Never live PANs; synthetic numbers only |
| BIN + last 4 | Allowed for display | Fine as masked reference data |
| Cardholder name, expiry | Protected when stored with the PAN | Synthetic values |
| CVV / CVC / CID | Never after authorization | Never stored; type test values into forms |
| Gateway tokens / customer IDs | Allowed | Use sandbox tokens only — production tokens don’t work in sandboxes |
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.
# 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.
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:
- Run the masking job in production (or in a CDE-scoped copy), never on a staging server.
- 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.
- Replace gateway tokens with sandbox tokens; production tokens are useless in a sandbox anyway.
- Scan the exported dump for PANs before it leaves (see the scanner below), and fail the job on any hit.
- 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.
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:
- Pattern: 13–19 digits, optionally grouped by spaces or dashes.
- Luhn: about one in ten random digit strings passes, so this removes roughly 90% of noise.
- 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.
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
| Check | How to verify |
|---|---|
| No live PANs in any non-production database | Run the scanner over a schema-wide export; review free-text columns |
| Card entry uses gateway-hosted fields or SDK | Inspect network requests: card data should go to the gateway, not your API |
| Server-side tests use gateway test tokens | Grep the test suite for raw card numbers outside an allowlist |
| Logs redact card numbers | Unit-test the redaction filter with test PANs in several formats |
| No CVV stored anywhere | Search schemas and event payloads for cvv, cvc, cid, security_code |
| Admin screens show last 4 only | Review 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 only | Document 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
- PCI DSS v4.0.1 Requirement 6.5.5 (requirement text, Cloudaware reference)
- PCI DSS v4.0.1 Requirement 6.5.6 (requirement text, Cloudaware reference)
- PCI SSC FAQ 1492 — masking and truncation (Requirements 3.4.1, 3.5.1)
- PCI SSC FAQ 1280 — card verification codes for card-on-file or recurring payments
- 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.