All 24 checks with why-it-matters prose, severity, and cross-references to related audits.
A Stripe secret key (`sk_live_*`) exposed in a client-side bundle or a `NEXT_PUBLIC_` environment variable is readable by every visitor who opens DevTools. An attacker can use it to create refunds, list customers, retrieve stored card fingerprints, or cancel subscriptions without touching your server. PCI-DSS 4.0 Req 3.4 prohibits storing or transmitting secret key material outside controlled server environments. OWASP A02 (Cryptographic Failures) flags this as one of the highest-severity credential exposures because the key grants API-level access to your entire Stripe account — charges, payouts, and customer data — to any actor who extracts it from your bundle.
Why this severity: Critical because a leaked secret key grants full Stripe account API access to anyone who reads the client bundle, enabling unauthorized refunds, payouts, and customer data exfiltration.
ecommerce-payment-security.payment-integration.no-secret-keys-clientSee full patternWithout webhook signature verification, any HTTP client on the internet can POST a fabricated `payment_intent.succeeded` event to your webhook endpoint and trigger order fulfillment, subscription activation, or credit grants — without paying anything. CWE-345 (Insufficient Verification of Data Authenticity) and CWE-352 (CSRF) both apply: an attacker replays or forges events, and your server acts on them as if they came from Stripe. PCI-DSS 4.0 Req 6.4 requires that payment event handling be protected against unauthorized manipulation. OWASP A07 (Identification and Authentication Failures) flags accepting unverified events as a complete authentication bypass.
Why this severity: Critical because an unverified webhook endpoint lets any actor forge a payment confirmation event and receive goods or services without being charged.
ecommerce-payment-security.payment-integration.webhook-signature-verifySee full patternWhen 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.
Why this severity: 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.
ecommerce-payment-security.payment-integration.no-raw-cards-backendSee full patternServer-side tokenization means raw card data — number, expiry, CVC — travels from the user's browser to your server before it reaches Stripe. Even if the server immediately tokenizes and discards the values, your application, reverse proxies, error trackers, and application logs all have an opportunity to capture that data. PCI-DSS 4.0 Req 3.3 prohibits this transit, and Req 4.2 requires that cardholder data in transit be protected with strong cryptography. Under OWASP A02 (Cryptographic Failures), passing card numbers over any server path you control — even encrypted — expands your attack surface and audit scope. The browser-side Stripe SDK sends card data directly from the user to Stripe's servers via an iframe, meaning your JavaScript context never sees the values.
Why this severity: Critical because server-side tokenization routes card numbers through your own infrastructure, putting you in PCI SAQ D scope and creating exfiltration risk in logs, APM agents, and proxy layers.
ecommerce-payment-security.payment-integration.tokenization-client-sideSee full patternA hardcoded Stripe secret key (`sk_live_*`) in source code becomes a permanent fixture of your git history. Even if you delete the line in a later commit, the key is recoverable by anyone with read access to the repository — including all historical collaborators, any connected CI/CD system, and anyone who ever cloned the repo. CWE-798 (Use of Hard-coded Credentials) and PCI-DSS 4.0 Req 3.6 (protection of cryptographic keys) both treat this as a critical violation. OWASP A07 (Identification and Authentication Failures) flags hardcoded credentials as a root-cause pattern for account takeover. The only safe recovery from a hardcoded secret is immediate key rotation — the exposed key must be treated as permanently compromised.
Why this severity: High because a hardcoded key is permanently embedded in git history, meaning rotation is required the moment it is detected and any delay leaves the Stripe account accessible to repository readers.
ecommerce-payment-security.payment-integration.keys-from-envSee full patternWithout idempotency keys, a network timeout on a charge request leaves your app in an ambiguous state: the payment may or may not have succeeded, and a retry creates a second charge for the same order. This is a real operational failure — customers who retry a checkout after a timeout get double-charged, support costs spike, and refund rates increase. CWE-362 (Race Condition) captures the concurrent-retry scenario that idempotency keys prevent. Beyond customer impact, double charges on high-volume stores compound into significant financial reconciliation problems. An idempotency key ensures that Stripe treats any retry of the same logical transaction as a no-op, returning the original result instead of creating a new charge.
Why this severity: High because missing idempotency keys on charge calls make double-charging customers a near-certainty under any network instability or client retry logic.
ecommerce-payment-security.payment-integration.idempotency-keys-all-chargesSee full patternCustom `<input>` fields for card number, expiry, or CVC mean card data exists in your JavaScript memory and the DOM — accessible to any browser extension, injected third-party script, or XSS payload running on the page. Official provider SDK components (Stripe Elements, Square Web Payments) render inside sandboxed iframes that your JavaScript cannot read. PCI-DSS 4.0 Req 4.2 requires that cardholder data be protected during transmission; CWE-319 (Cleartext Transmission) and CWE-829 (Inclusion of Functionality from Untrusted Control Sphere) both apply when card data exists in a script-accessible DOM. OWASP A02 (Cryptographic Failures) treats unencrypted card data in the browser JavaScript context as a direct exfiltration risk.
Why this severity: High because custom card inputs expose card data to your entire JavaScript context, making it readable by any XSS payload or browser extension regardless of downstream tokenization.
ecommerce-payment-security.payment-integration.official-sdks-onlySee full patternPayment tokens (`pm_*`, `tok_*`, `pi_*`) stored in `localStorage` or `sessionStorage` are readable by any JavaScript on the page — same-origin scripts, browser extensions, and XSS payloads all have full access. PCI-DSS 4.0 Req 3.3 prohibits storing sensitive authentication data after authorization; CWE-312 (Cleartext Storage of Sensitive Information) and CWE-922 (Insecure Storage of Sensitive Information) both apply. OWASP A02 (Cryptographic Failures) flags browser storage as an insecure persistence layer for payment credentials. Unlike session cookies, `localStorage` has no expiry and persists across browser restarts, meaning a compromised token can be extracted days or weeks after the initial checkout.
Why this severity: High because payment tokens persisted in browser storage are indefinitely accessible to any JavaScript on the page, including XSS payloads and browser extensions.
ecommerce-payment-security.client-side-handling.no-storage-sensitive-dataSee full patternCheckout pages accessed over plain HTTP expose payment form data — billing address, card tokenization requests, and order details — to network observers and man-in-the-middle attackers. PCI-DSS 4.0 Req 4.2 mandates that cardholder data in transit be protected with strong cryptography; CWE-319 (Cleartext Transmission of Sensitive Information) and CWE-523 (Unprotected Transport of Credentials) directly apply. OWASP A02 (Cryptographic Failures) treats unencrypted checkout traffic as a critical failure. Even if the card number itself is tokenized via an iframe, HTTP exposes billing address fields, email, and order totals — enough for targeted phishing or order-manipulation attacks.
Why this severity: High because HTTP-accessible checkout pages expose billing details and order data to network observers, violating PCI-DSS 4.0 Req 4.2 and creating a man-in-the-middle attack surface.
ecommerce-payment-security.client-side-handling.https-enforcedSee full patternSkipping 3D Secure (SCA) challenge handling means your checkout silently fails for European customers (where SCA is legally mandated under PSD2) and for any card issuer that requires additional authentication. More critically, omitting the `requires_action` status check means your code can mark an order as paid after receiving the initial PaymentIntent — before the card issuer has actually authorized the charge. The result: inventory is decremented, fulfillment is triggered, and the customer receives their order, but the actual charge later fails when the 3DS challenge times out. PCI-DSS 4.0 Req 8.4 covers multi-factor authentication requirements; OWASP A07 (Identification and Authentication Failures) captures incomplete authentication flows.
Why this severity: High because ignoring the `requires_action` state causes your backend to mark orders as paid before authentication completes, enabling fulfillment of unpaid orders on 3DS-required cards.
ecommerce-payment-security.client-side-handling.three-d-secure-scaSee full patternPayment endpoints without rate limiting are the primary target of automated carding attacks: bots enumerate stolen card numbers by submitting hundreds of small charges per minute, using your checkout as a free card-validity oracle. Each probe costs you a Stripe fee ($0.05–$0.30 per declined charge), triggers fraud signals on your account, and can result in Stripe account suspension. CWE-770 (Allocation of Resources Without Limits or Throttling) captures this exact attack vector. OWASP A07 (Identification and Authentication Failures) covers the authentication-bypass aspect of unauthenticated brute-force. PCI-DSS 4.0 Req 6.4 requires that web-facing applications be protected against known attack patterns including automated attacks.
Why this severity: High because unthrottled payment endpoints are directly exploitable for automated carding attacks that accumulate Stripe fees, fraud signals, and potential account suspension.
ecommerce-payment-security.client-side-handling.rate-limiting-payment-endpointsSee full patternWhen a Stripe charge succeeds but the subsequent database write fails, you have taken the customer's money without creating their order record. Without compensation logic, this is a silent failure: your system shows no order, the customer sees no confirmation, and no automated process triggers a refund or retry. CWE-362 (Race Condition / Improper Synchronization) applies when the payment and fulfillment steps are not treated as an atomic operation. In high-volume stores, even a 0.1% database failure rate translates to dozens of unreconciled charges per day — each requiring manual support intervention, chargeback risk, and reputational damage.
Why this severity: High because an unhandled post-payment database failure charges the customer without creating their order, requiring manual intervention for every occurrence and creating chargeback exposure.
ecommerce-payment-security.client-side-handling.post-payment-rollbackSee full patternA payment submit button that stays enabled during an in-flight request lets users double-click or rapidly resubmit, dispatching concurrent payment requests for the same cart. Even if you use idempotency keys on the backend, two concurrent requests before the first response arrives may each generate a separate key — resulting in two distinct charges. Beyond accidental double-clicks, an enabled button during processing provides no user feedback, increasing anxiety and intentional resubmission. CWE-362 (Race Condition) and CWE-799 (Improper Control of Interaction Frequency) both apply. The fix is a two-layer guard: UI state disabling the button immediately on first submission, and backend idempotency preventing duplicates if the UI guard is bypassed.
Why this severity: High because an always-enabled submit button under network latency allows concurrent submissions that can produce duplicate charges before the idempotency layer has a chance to deduplicate them.
ecommerce-payment-security.client-side-handling.duplicate-payment-preventionSee full patternLogging a full Stripe error object or a payment form's `event.target` captures card BIN data, error responses that reference card numbers, and provider-internal identifiers — all of which end up in browser consoles, error tracking tools, and analytics pipelines. PCI-DSS 4.0 Req 3.3 prohibits retaining sensitive authentication data; CWE-312 (Cleartext Storage of Sensitive Information) and CWE-532 (Insertion of Sensitive Information into Log File) both apply. OWASP A09 (Security Logging and Monitoring Failures) captures the dual failure: bad logging that records sensitive data while simultaneously failing to record operationally useful signals. A Sentry breadcrumb that captures form data can expose card numbers in your error dashboard to any team member with access.
Why this severity: Medium because payment-related console logs and analytics events can capture card BIN data and provider error details, creating PCI-scope violations in browser consoles and third-party monitoring tools.
ecommerce-payment-security.fraud-prevention.no-card-loggingSee full patternWithout billing address data, the card network's Address Verification System (AVS) cannot run a fraud check on the transaction. AVS compares the postal code and street address the customer provides against the address on file with the card issuer — a mismatch is one of the strongest signals that a card is being used fraudulently. Skipping AVS doesn't just reduce fraud detection accuracy; it increases your Stripe Radar risk score across all transactions and can raise your effective fraud dispute rate. PCI-DSS 4.0 Req 6.4 covers fraud detection controls; missing AVS data is a direct gap in that control layer for card-present and card-not-present transactions alike.
Why this severity: Medium because omitting billing address data disables AVS fraud scoring at the card network level, increasing the success rate of fraudulent card-not-present transactions against your store.
ecommerce-payment-security.fraud-prevention.avs-enabledSee full patternCVC verification is a card-not-present fraud control: the three- or four-digit code proves the cardholder has physical access to the card, since CVC is never stored by merchants after authorization and is not present on card magnetic stripes. Disabling CVC verification — whether by removing the CVC field from the checkout form or by configuring Stripe Radar to allow `cvc_check != 'pass'` — removes this signal entirely. The practical result is that stolen card numbers without physical card access become viable for fraudulent purchases. PCI-DSS 4.0 Req 8.3 covers authentication factors for payment flows; CWE-287 (Improper Authentication) captures the failure mode when a required authentication element is bypassed.
Why this severity: Medium because explicitly disabling CVC verification removes a core card-not-present fraud signal, making the store viable for stolen-card attacks where the attacker lacks physical card access.
ecommerce-payment-security.fraud-prevention.cvc-verification-enabledSee full patternReturning raw exception messages, Stripe error objects, or database errors from payment API routes leaks internal system architecture to every browser that inspects network responses. A response like `{ error: "No such payment_intent: 'pi_3Ox...'" }` tells an attacker your payment intent ID format, your Stripe account configuration, and the exact server-side path that failed. CWE-209 (Generation of Error Message Containing Sensitive Information) directly names this pattern. OWASP A05 (Security Misconfiguration) flags information leakage from unhandled exceptions as a reconnaissance vector. Beyond security, raw Stripe error messages contain decline codes and internal identifiers that help card-testing bots distinguish working cards from flagged ones.
Why this severity: Medium because raw exception messages in API responses expose internal system details and Stripe error internals that serve as reconnaissance data for card-testing attacks and targeted exploits.
ecommerce-payment-security.fraud-prevention.generic-error-messagesSee full patternStripe's `decline_code` field on a `StripeCardError` carries values like `stolen_card`, `fraudulent`, `card_velocity_exceeded`, and `insufficient_funds`. If your API forwards this code to the browser, you are giving card-testing bots a real-time oracle: they can distinguish cards that are blocked as stolen from cards that are simply declined for insufficient funds, and continue probing with the latter. CWE-209 (Information Exposure Through Error Message) and CWE-200 (Exposure of Sensitive Information) both apply. OWASP A05 (Security Misconfiguration) covers this as information leakage. The internal `decline_code` is a legitimate signal for your fraud team — it should flow to your logging pipeline, not to the client.
Why this severity: Medium because exposing decline codes to clients gives card-testing bots a free oracle to distinguish stolen-and-blocked cards from valid-but-declined ones, improving attack efficiency.
ecommerce-payment-security.fraud-prevention.decline-codes-internal-onlySee full patternStripe's webhook delivery has a 30-second timeout per attempt. If your webhook handler performs multiple sequential database writes, sends email via an external API, updates inventory, and calls third-party fulfillment services before returning 200, the total latency can easily exceed 30 seconds under normal load — and will exceed it under any database contention or external service slowdown. Stripe then marks the delivery as failed and retries, causing your handler to run again on the same event. Without idempotency guards, these retries create duplicate orders, duplicate emails, and duplicate inventory decrements. CWE-400 (Uncontrolled Resource Consumption) captures the timeout-induced retry cascade.
Why this severity: Low because excessive synchronous processing before returning 200 causes Stripe to retry the webhook, and without idempotency guards those retries produce duplicate orders and emails.
ecommerce-payment-security.payment-errors.webhook-quick-responseSee full patternStripe retries webhook delivery up to 3 days when it doesn't receive a 200 response within 30 seconds. If your webhook handler performs a plain `db.orders.insert()` keyed only on customer data — not on `event.id` or `paymentIntent.id` — every retry creates a duplicate order record, triggers a duplicate fulfillment, and sends the customer a duplicate confirmation email. CWE-362 (Race Condition) applies when two concurrent deliveries of the same event race to insert the same order. This is not a theoretical failure: network blips during high-traffic events (flash sales, holiday peaks) cause Stripe retries at exactly the moment your database is under maximum load.
Why this severity: Low because non-idempotent webhook handlers create duplicate orders and emails on every Stripe retry, with retry frequency increasing precisely when database load causes initial delivery to timeout.
ecommerce-payment-security.payment-errors.webhook-idempotent-processingSee full patternA declined payment that locks the checkout form, redirects to a dead-end error page, or crashes the component turns a recoverable event into an abandoned cart. Customers whose first card triggers a fraud hold or hits an insufficient-funds block cannot switch to a second card without restarting the entire flow, which inflates checkout abandonment on an experience already covered by user-experience and error-resilience taxons. For high-ticket orders this converts one soft decline into lost revenue and a support ticket, and repeated decline-and-reload loops can trip Stripe Radar rules that escalate future attempts to hard blocks.
Why this severity: Low because the control has no security or data-integrity impact; the damage is conversion loss and friction, not exposure or fraud.
ecommerce-payment-security.payment-errors.graceful-decline-handlingSee full patternFailed payment attempts are the primary signal for detecting carding attacks, credential stuffing, and account takeover attempts on your checkout. Without server-side audit logs, you cannot identify patterns — for example, 500 failed attempts against different card numbers from the same IP over one hour, or repeated failures on the same user account from different countries. PCI-DSS 4.0 Req 10.2 mandates logging of access control failures and user authentication attempts. CWE-778 (Insufficient Logging) and OWASP A09 (Security Logging and Monitoring Failures) both target exactly this gap. Client-side `console.log` calls are ephemeral and invisible to your backend — they don't count as audit logging.
Why this severity: Low because absent server-side payment failure logs eliminate the primary data source for detecting carding attacks, making fraud patterns invisible until they escalate to chargebacks.
ecommerce-payment-security.payment-errors.failed-payment-audit-logSee full patternPayment providers require amounts in the smallest currency unit — cents for USD, pence for GBP — but zero-decimal currencies (JPY, KRW, VND, and 13 others) are already expressed in their smallest unit and must not be multiplied by 100. A checkout that applies `price * 100` to a JPY price of ¥1,000 submits a charge for ¥100,000 — a 100x overcharge. CWE-681 (Incorrect Conversion between Numeric Types) captures this integer arithmetic error. The ISO 4217 functional correctness requirement means using the wrong currency code (e.g., `"JP"` instead of `"jpy"`) will also cause the payment to be rejected or processed in the wrong currency. This is a silent production defect: it passes all tests that mock the Stripe API but fails live with real amounts.
Why this severity: Low because zero-decimal currency miscalculation causes 100x overcharges on live transactions for affected currencies, and invalid currency codes cause silent payment failures.
ecommerce-payment-security.payment-errors.correct-currency-codesSee full patternWhen `statement_descriptor` or `statement_descriptor_suffix` is omitted, charges fall back to the Stripe account's legal name — often a holding company or DBA that the customer does not recognize on their bank statement. Unrecognized line items drive chargebacks filed as 'fraudulent transaction' even when the purchase was legitimate, and excess chargeback ratios escalate Stripe Radar risk scoring, raise processing fees, and can trigger account review. This hits the user-experience and content-integrity taxons directly: the customer sees a string they cannot tie back to your store, and the dispute is decided against you regardless of delivery proof.
Why this severity: Low because the issue surfaces as chargeback friction and post-purchase confusion rather than data loss or takeover.
ecommerce-payment-security.payment-errors.statement-descriptorsSee full patternRun this audit in your AI coding tool (Claude Code, Cursor, Bolt, etc.) and submit results here for scoring and benchmarks.
Open Payment Security Audit