When raw card numbers (PAN), expiry dates, or CVC codes pass through your server, you become a PCI DSS SAQ D merchant — the most burdensome compliance tier, requiring quarterly penetration tests, annual on-site audits, and over 200 individual controls. PCI-DSS 4.0 Req 3.3 prohibits storing sensitive authentication data after authorization, and Req 4.2 prohibits transmitting cardholder data over open networks without strong encryption. OWASP A02 (Cryptographic Failures) treats cleartext card transit as a critical data exposure. Beyond compliance, any server-side log, crash report, or debug middleware that captures request bodies will inadvertently record card numbers — a single data breach then exposes millions of records.
Critical because routing raw card numbers through your server places you in PCI SAQ D scope and creates multiple exfiltration vectors — logs, APM tools, error trackers, and database query logs can all capture card data in transit.
Use Stripe Elements or the PaymentElement to tokenize card data in the browser. Your server should only ever receive the opaque pm_xxx or pi_xxx identifier.
// WRONG — card data sent to your server
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ cardNumber: '4242...', cvc: '123', expMonth: 12, expYear: 2026 })
})
// CORRECT — only the token reaches your server
const { paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement)!,
})
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ paymentMethodId: paymentMethod.id })
})
Audit every API route handler that accepts payment or checkout POST bodies — none of them should have fields named cardNumber, pan, cvc, cvv, expiry, or expMonth.
ID: ecommerce-payment-security.payment-integration.no-raw-cards-backend
Severity: critical
What to look for: Count every API route handler that processes payment or checkout requests. For each, enumerate the request body parameters and classify them as tokenized (e.g., paymentMethodId, nonce, token) or raw card data (e.g., cardNumber, card_number, pan, expiry, expMonth, cvc, cvv, securityCode). Check for any fetch() or axios.post() calls in checkout components that serialize card field values directly into request bodies. Also check GraphQL mutations and form action attributes that post to backend routes — verify what data they include.
Pass criteria: The backend never receives raw card data in any form. No more than 0 API endpoints should accept card number, expiry, or CVC parameters. The only payment-related data passed from client to server is a tokenized reference (a paymentMethod, paymentIntent, nonce, or similar opaque identifier returned by the provider's SDK after client-side tokenization).
Fail criteria: Any backend API endpoint accepts raw card numbers, expiry dates, or CVC codes as request parameters ��� regardless of whether the endpoint actually uses them.
Skip (N/A) when: The project uses only a fully hosted payment page operated by the provider (e.g., Stripe Hosted Checkout, PayPal Standard) where no card data ever touches your code.
Detail on fail: Describe the exposure precisely. Example: "POST /api/checkout accepts { cardNumber, expMonth, expYear, cvc } in request body — raw card data passes through your server, placing you in scope for PCI DSS SAQ D"
Remediation: Your backend should only ever receive a token, never card data. The correct flow is:
Client: user enters card into Stripe Elements
→ Stripe.js sends card data directly to Stripe servers
→ Stripe returns a paymentMethod ID to your client
Client: sends { paymentMethodId: 'pm_xxx' } to your backend
Backend: uses paymentMethodId to confirm the PaymentIntent
// WRONG — client sending card data to your server
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ cardNumber: '4242...', cvc: '123' })
})
// CORRECT — client only sends the token
const { paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: elements.getElement(CardElement)! })
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ paymentMethodId: paymentMethod.id })
})