Collecting raw card numbers, CVV, or expiry dates in your own form and submitting them to your server puts you directly in scope for PCI DSS compliance (pci-dss:4.0 Req 4.2.1, Req 6.4.3). That compliance burden means annual audits, penetration tests, and strict infrastructure controls — or, if you skip it, catastrophic liability when a breach occurs. OWASP A05 (Security Misconfiguration) covers exactly this failure mode: trusting that a payment provider SDK is installed is not the same as using it. Every payment flow that touches raw card data on your server is an exposure vector.
Critical because raw card data transiting your server creates direct PCI DSS scope and breach liability with no mitigating controls.
Replace any custom card input fields with your payment provider's hosted element. Card data must go directly to the provider's domain — your server should only ever receive opaque tokens or session IDs.
For Stripe, swap the custom form for PaymentElement:
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'
// Card data is captured by Stripe's iframe — never touches your server
// Your API receives only the paymentIntentId returned by stripe.confirmPayment()
Verify the fix by opening your browser's network tab during checkout: card field submissions should go to api.stripe.com, not your own domain.
ID: saas-billing.payment-security.pci-compliant-provider
Severity: critical
What to look for: Examine how payment information is collected and processed. Look for Stripe.js / Stripe Elements / Stripe Checkout, PaymentElement, Paddle.js, LemonSqueezy embedded checkout, Braintree hosted fields, or similar SDK-based payment collection. The critical signal is whether raw card data ever passes through your server. Check all form components, API routes, and checkout flows for direct card number fields (type="tel" or type="number" inputs named card, cardNumber, cvv, cvc, expiry) that are submitted to your own backend. Beyond package presence, verify that Stripe Elements or Checkout is actually imported and used: look for imports of Elements, CardElement, or PaymentElement from @stripe/react-stripe-js, or server-side calls to stripe.checkout.sessions.create() / redirectToCheckout in API routes.
Pass criteria: Count every payment form and checkout flow in the codebase. Payment collection uses an SDK-hosted element (Stripe Elements, Stripe Checkout, Paddle overlay, LemonSqueezy checkout) that handles card data entirely on the provider's side, AND there is evidence of actual usage — imports of Elements, CardElement, or PaymentElement from @stripe/react-stripe-js, or server-side calls to stripe.checkout.sessions.create() or redirectToCheckout in API routes. Your server never receives raw card data — only payment intent IDs, session IDs, or tokens. At least 1 import of Elements, CardElement, or PaymentElement must be present. Having @stripe/stripe-js in package.json alone is not sufficient to pass.
Fail criteria: Your application collects raw card numbers, CVV, or expiry dates in a form that submits to your own server, even if you then forward to a payment provider. Any server-side handling of raw card data fails this check. Do NOT pass if a hosted element is present alongside a custom card form — the custom form is the vulnerability.
Skip (N/A) when: No payment functionality detected in this project. Signal: no payment provider SDK in package.json and no billing-related API routes.
Cross-reference: For API security beyond payment handling, the API Security audit covers authentication, authorization, and input validation across all endpoints.
Detail on fail: Describe what was found. Example: "Custom payment form in components/checkout/CardForm.tsx collects card number and CVV fields submitted to /api/billing/charge — raw card data passes through application server"
Remediation: Handling raw card data yourself requires PCI DSS compliance, which is a significant ongoing audit burden. Use your payment provider's hosted elements instead:
For Stripe, replace custom card inputs with Stripe Elements or Stripe Checkout:
// Use Stripe's hosted PaymentElement — card data never touches your server
import { PaymentElement } from '@stripe/react-stripe-js'
// Your server only receives a paymentIntentId, never card data
For other providers, use their equivalent hosted checkout or embedded payment widgets. Verify by checking your network tab during checkout — you should see card data going directly to the provider's domain (stripe.com, paddle.com, etc.), not to your own API.