Custom <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.
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.
Replace all custom card <input> elements with the provider's official SDK components. Stripe Elements renders in an isolated iframe your JavaScript cannot access.
// WRONG — card data accessible in your JS context
<input name="cardNumber" placeholder="Card Number" />
<input name="expiry" placeholder="MM/YY" />
<input name="cvc" placeholder="CVC" />
// CORRECT — Stripe's sandboxed iframe
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
function PaymentForm() {
const stripe = useStripe()
const elements = useElements()
// stripe.createPaymentMethod reads from the iframe — never touches your DOM
return <CardElement />
}
Search checkout components for <input> elements with name, id, or placeholder containing "card", "number", "cvc", "cvv", "expiry", or "mm" and replace every match.
ID: ecommerce-payment-security.payment-integration.official-sdks-only
Severity: high
What to look for: Enumerate all card input mechanisms in checkout components. For each, classify as official SDK (e.g., <CardElement>, <PaymentElement>, <Elements> wrapper from @stripe/react-stripe-js; the Square Web Payments SDK card object; the PayPal Buttons component) or custom input (e.g., <input> elements with names or placeholders suggesting card data: "Card Number", "card-number", "cardNumber", "CVV", "CVC", "expiry", "mm/yy"). Count the total of each type.
Pass criteria: All card data capture is performed through official provider UI components that render in isolated iframes (Stripe Elements, PaymentElement, Square Web Payments, PayPal Buttons). No more than 0 custom input elements should capture card field data directly.
Fail criteria: The checkout form contains custom <input> fields for card number, expiry, or CVC — regardless of any downstream tokenization logic. Custom inputs outside provider iframes mean the card data touches your JavaScript context.
Do NOT pass when: A custom input field captures card data and then passes it to a provider SDK for tokenization — the card data still exists in your JavaScript memory and DOM, making it accessible to browser extensions and XSS attacks. This does not count as pass even though tokenization eventually happens.
Skip (N/A) when: The project uses a fully hosted payment page (Stripe Hosted Checkout, PayPal Checkout) where no card inputs exist in your codebase at all.
Detail on fail: Describe the custom inputs found. Example: "components/checkout/CardForm.tsx uses three custom <input> fields for card number, expiry, and CVC — card data is accessible in your JavaScript context and to any browser extensions"
Remediation: Replace custom inputs with the provider's official SDK components, which render in sandboxed iframes your JavaScript cannot read:
// CORRECT — using Stripe Elements (renders in a secure Stripe-owned iframe)
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
export function CheckoutForm() {
return (
<Elements stripe={stripePromise}>
<PaymentForm />
</Elements>
)
}
function PaymentForm() {
const stripe = useStripe()
const elements = useElements()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { paymentMethod } = await stripe!.createPaymentMethod({
type: 'card',
card: elements!.getElement(CardElement)!,
})
// Send paymentMethod.id to your server
}
return (
<form onSubmit={handleSubmit}>
<CardElement /> {/* Stripe's secure iframe — your JS cannot read card data */}
<button type="submit">Pay</button>
</form>
)
}