Server-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.
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.
Tokenization must happen in the browser using the Stripe JS SDK. The server SDK's paymentMethods.create() with raw card fields is the anti-pattern to eliminate.
// WRONG — server-side tokenization (card data touches your server)
// app/api/payment/route.ts
const pm = await stripe.paymentMethods.create({
type: 'card',
card: { number: body.cardNumber, exp_month: body.expMonth, cvc: body.cvc }
})
// CORRECT — client-side tokenization only
// In a client component
const { paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement)!,
})
// Send paymentMethod.id to your server — never the card fields
Search for stripe.paymentMethods.create and stripe.tokens.create in server-side files. Every occurrence with a raw card object is a violation.
ID: ecommerce-payment-security.payment-integration.tokenization-client-side
Severity: critical
What to look for: Enumerate all tokenization calls across the codebase. For each call, classify it as client-side (browser SDK: stripe.createToken(), stripe.createPaymentMethod(), elements.submit()) or server-side (Node SDK: stripe.tokens.create(), stripe.paymentMethods.create() with raw card object parameters). Count the total tokenization calls and report the ratio of client-side to server-side. Trace API routes that accept card data and immediately call the provider to tokenize — this is server-side tokenization.
Pass criteria: 100% of tokenization calls are made from the client browser using the provider's client-side JavaScript SDK. No more than 0 server-side tokenization calls should exist. The server SDK is only used to confirm, capture, or retrieve previously tokenized payment methods.
Fail criteria: Any server-side code creates tokens or payment methods by passing raw card data to the provider's server SDK. Even if the intent is to immediately discard the raw data, this pattern means card data transits your server.
Skip (N/A) when: The project uses a fully hosted payment form (Stripe Hosted Checkout, PayPal Standard) where tokenization is handled entirely outside your code.
Detail on fail: Describe the server-side tokenization pattern. Example: "app/api/payment/route.ts calls stripe.paymentMethods.create({ type: 'card', card: { number, exp_month, exp_year, cvc } }) — tokenization is happening server-side with raw card data"
Remediation: Tokenization must happen in the browser, never on your server:
// CORRECT — client-side tokenization
// In a client component using @stripe/react-stripe-js
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'
const stripe = useStripe()
const elements = useElements()
const { paymentMethod, error } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement)!,
})
// Send only paymentMethod.id to your backend
await fetch('/api/confirm-payment', {
method: 'POST',
body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
})
The server only ever sees the opaque pm_xxx identifier — never card details.