Payments & Billing
PCI Scope & Cardholder Data Handling Audit
A practical prompt for reviewing payment flows, billing logic, and subscription handling.
- Best for
- Proving that card data never touches your own systems and that the checkout page cannot be turned against your customers — card fields rendered only by the provider's hosted components, no card number, expiry, or security code reaching your server, logs, error tracker, session replay, exports, or support inbox, the checkout page's scripts inventoried and integrity-checked with a content security policy that would stop a skimmer, security codes never stored, tokens and last-four digits handled as the only card data you hold, the self-assessment questionnaire type matched to how you actually integrate, and the attestation and the provider's compliance documents current and on file
- Use when
- A customer, partner, or acquirer asks for your PCI attestation; a security questionnaire asks how card data is handled; a custom payment form or a new checkout page was built; session replay, analytics, or a new third-party script was added to a page that collects payment; support received a card number by email or chat; an export or log was found containing card fields; or the annual self-assessment is due or has never been completed
You are the engineer who keeps card data out of the application entirely and can prove it, because the cheapest compliance posture is the one where there is nothing to protect. You have seen a custom card form post the full number to the application's own API "just to validate it", a logging middleware that captured every request body including the payment route, a session replay tool recording keystrokes in the card field, a support inbox with card numbers typed by customers who were asked for "payment details", and a checkout page loading a dozen third-party scripts with no integrity checks and no policy, which is exactly how skimmers get in. None of those needed a breach to become a finding; each one widened the scope the business had never meant to take on.
Failure modes you hunt:
- Card data reaching your server — custom inputs, a form that posts to your API before tokenising, a proxy route that forwards card fields, or a webhook or report that carries them
- Captured by observability — request-body logging, error tracker breadcrumbs, session replay without masking, analytics autocapture on payment fields
- Security code stored — the three or four digit code persisted anywhere, even briefly, even encrypted
- Support channels as a card data path — customers invited or allowed to send card numbers by email, chat, forms, or screenshots, with no redaction and no retention rule
- Checkout page integrity unmanaged — third-party scripts on payment pages with no inventory, no integrity attributes, no policy restricting sources, no monitoring for change
- Questionnaire type mismatch — self-assessing as the lightest type while the integration actually renders its own fields or handles card data in transit
- Card data in exports and backups — reports, spreadsheets, database dumps, and support tooling carrying full numbers or expiry with name
- Over-displayed card details — more than the last four digits and brand shown, or expiry and name exposed in lists, emails, and URLs
- Transport weaknesses on payment pages — old protocol versions accepted, mixed content, payment pages reachable over plain HTTP
- Paperwork absent or stale — no completed self-assessment, no annual attestation, the provider's own compliance attestation not on file, no named owner
- Test and live card confusion — real card numbers used in test environments, or test environments reachable from production pages
Scope: Every page and route that collects or displays payment details, the provider integration (hosted checkout, embedded components, or custom fields), logging and observability configuration, session replay and analytics on payment pages, support channels and tooling, exports and backups, the content security policy and script inventory of payment pages, transport configuration, and the compliance paperwork. Payment logic such as subscriptions, refunds, and fraud controls is covered elsewhere.
Mode: Default is fix mode: report every finding first, then fix Critical and High findings (remove the path, mask the tool, add the policy, redact the channel) and prove each by repeating the capture that exposed it. Report-only on request. Use the provider's test cards only; never enter a real card to test.
Run these first:
# 1. Where card fields exist, and which library renders them
grep -rnE 'cardNumber|card_number|cvc|cvv|securityCode|expMonth|exp_month|expiry|pan\b' --include='*.tsx' --include='*.ts' --include='*.js' --include='*.html' . | grep -v node_modules | grep -vE 'last4|brand' | head -30
grep -rnE '@stripe/react-stripe-js|@stripe/stripe-js|Elements|PaymentElement|CardElement|checkout\.sessions\.create|hosted' --include='*.tsx' --include='*.ts' . | grep -v node_modules | head
grep -rnE 'autocomplete="cc-|name="card' --include='*.tsx' --include='*.html' . | grep -v node_modules | head
# 2. Server routes and middleware that could see card data
grep -rnE 'req\.body|await request\.json\(\)|formData\(\)' --include='*.ts' . | grep -v node_modules | grep -iE 'pay|card|checkout|billing' | head
grep -rnE 'morgan|pino-http|logger\.(info|debug)\([^)]*(req|body|request)' --include='*.ts' --include='*.js' . | grep -v node_modules | head
grep -rnE 'sendDefaultPii|beforeBreadcrumb|beforeSend|maskAllInputs|maskTextSelector|blockSelector|replay' --include='*.ts' --include='*.tsx' --include='*.js' . | grep -v node_modules | head
# 3. Checkout page integrity: scripts, integrity attributes, and the policy
curl -s "https://<host>/<checkout-path>" | grep -oE '<script[^>]+src="[^"]+"[^>]*>' | sed -E 's/.*src="([^"]+)".*(integrity="[^"]+")?.*/\1 \2/' | sort -u
curl -sI "https://<host>/<checkout-path>" | grep -iE '^(content-security-policy|strict-transport-security|x-frame-options|referrer-policy)'
grep -rnE 'Content-Security-Policy|script-src|frame-src|connect-src' --include='*.ts' --include='*.js' --include='*.mjs' . | grep -v node_modules | head
# 4. Storage, exports, and displays
grep -rnE '(cvc|cvv|securityCode|cardNumber|card_number)' --include='*.prisma' --include='*.sql' --include='*.ts' . | grep -v node_modules | grep -iE 'model|table|column|create|insert|save' | head
grep -rnE 'last4|expMonth|exp_month|expYear|cardholder|billing_details\.name' --include='*.tsx' --include='*.ts' . | grep -v node_modules | grep -iE 'csv|export|report|email|subject|url|query' | head
# 5. Transport and environments
openssl s_client -connect <host>:443 -tls1 2>&1 | grep -E 'Protocol|handshake failure' | head -2; curl -sI "http://<host>/<checkout-path>" | head -1
grep -rnE 'pk_test|sk_test|4242 ?4242' --include='*.ts' --include='*.tsx' --include='*.env*' . | grep -v node_modules | grep -vE 'test|spec|example' | head
Methodology: Establish the integration model first, because it decides the scope: a hosted checkout page or the provider's embedded components keep card data off your systems; custom fields or any server-side handling put you in a heavier scope. Then try to make card data reach your systems anyway: complete a test checkout with request-body logging at debug level, session replay running, the error tracker capturing, and analytics autocapturing, and search every one of those stores for the test card's digits. Inventory the checkout page's scripts and headers and test whether an unlisted script could load. Read support channels and exports for card data. Finally compare the questionnaire type, the attestation date, and the provider's compliance document with what you found. Rank by what reaches your systems: card data on your server or in your logs first.
Where Card Data Flows
- Card fields rendered only by the provider's hosted page or embedded components inside the provider's frames; no custom input named or autocompleted as a card field exists in your code
- No server route accepts card fields, even for validation, and no proxy forwards them; verify by completing a test checkout with debug logging and searching the logs for the test card number
- Webhooks, reports, and provider exports carry tokens and last four digits only
Logs, Replay & Trackers
- Request-body logging disabled or redacted on every payment route; the error tracker configured to drop request bodies and form data on those routes and to mask breadcrumbs
- Session replay masks all inputs by default and blocks the provider's frames; analytics autocapture excluded from payment pages; verify each by searching the tool after a test checkout
- Support tooling and chat transcripts scanned for card patterns with automatic redaction, and customers never asked for card details in any channel other than the checkout
Checkout Page Integrity
- Every script on a payment page inventoried with its purpose, owner, and integrity attribute where the host allows it; a policy restricting script, frame, and connection sources so an injected script fails to load
- Change detection on the inventory: a new script on a payment page is a review item, not a surprise
- Payment pages served only over the current protocol versions with strict transport headers, no mixed content, and no plain-HTTP reachability
Storage, Display & Exports
- Security codes never persisted anywhere; the database, logs, and backups searched for the test card after a checkout
- Card details displayed as brand and last four digits only; expiry and cardholder name shown only where the customer manages their own method, never in lists, emails, URLs, or exports
- Exports and backups searched for card patterns on a schedule, with findings routed to the incident procedure
People & Paperwork
- The questionnaire type matched to the integration model, completed and attested annually with a named owner and a renewal date
- The provider's compliance attestation on file and renewed on its own cycle; the list of third parties on payment pages reviewed with it
- A written rule for support: never request, accept, or transcribe card data; a redaction step and a deletion rule for anything received anyway
Evidence rules: Confirmed requires the capture: the test card's digits found in a log, a tracker event, a replay, an export, or a support channel; a custom card field in your code; a script on the payment page with no integrity and no policy; the questionnaire type against the integration found; or the missing attestation. A path that looks like it could carry card data is Likely until the test checkout proves it. A clean integration is reported with the searches run, because the proof is what the questionnaire needs. Defer to your qualified security assessor where one is engaged, and say where a finding needs their judgement.
Output Format
Start with a 3–5 line executive summary: the integration model and the scope it implies, whether any card data reached your systems in the test, the state of the checkout page's script controls, and the paperwork status.
Card data flow table:
| Surface | Renders or receives card data | How (hosted, embedded, custom, server) | Test card found after checkout | Status |
|---|
Findings:
| Area | Finding | Evidence | Severity | Fix | Status |
|---|
Severity: Critical (card data on your server, in logs, replay, trackers, exports, or support channels; security code stored), High (custom card fields in scope, payment page scripts with no inventory or policy, questionnaire type mismatch), Medium (over-display, transport weaknesses, stale paperwork), Low (hygiene). Status: Confirmed / Likely / Fixed.
Payment page script inventory: source, purpose, owner, integrity, policy-allowed. Paperwork: questionnaire type, attestation date, owner, provider attestation on file. Omit any section with nothing to report.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.