Good Carder
Professional
- Messages
- 763
- Reaction score
- 493
- Points
- 63
Introduction: Three Fundamental Principles
A payment form is more than just a set of fields for entering card details. It's a digital boundary where real money, confidential information, and a business's reputation lie. A vulnerability here could lead to the leak of thousands of customers' payment data, mass carding, and regulatory fines amounting to millions of euros.A secure payment form is built on three fundamental principles that must be implemented in concert:
- Never touch raw payment data. Your server shouldn't see the card number, expiration date, or CVV/CVC. If this data ends up on your backend, you're already at risk.
- Control what's executed. Any script on a page is a potential skimmer. Disable unsafe sources, sign permitted scripts with Subresource Integrity, enforce a strict Content Security Policy without unsafe-inline or unsafe-eval, and implement Trusted Types to protect against DOM XSS.
- Protect against contextual attacks. If a payment form is embedded in an iframe on another website (clickjacking), an attacker can intercept input by replacing the visual layer.
These three principles are transformed into specific technical measures, which we will examine further.
Part 1. Protecting Against XSS with CSP and Trusted Types
Cross-Site Scripting (XSS) is one of the most common and dangerous vulnerabilities in web applications. An attacker who injects their script into a payment page can intercept entered card numbers and CVVs and send them to their server in real time. This turns any form, even the most modern one, into a tool for mass skimming.1.1 What is XSS and why is CSP a key defense?
DOM-based XSS occurs when code reads data from an attacker-controlled source (such as location.hash, document.referrer, or URLSearchParams) and writes it to the page in an unsafe manner. A general rule: never pass untrusted data to sinks that interpret HTML or execute code.Unsafe sinks to avoid with dynamic data include:
JavaScript:
// ❌ All these constructs can be executed by embedded scripts
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput, 0); // string form only
new Function(userInput)();
Content Security Policy (CSP) is a browser security mechanism that allows you to control which external sources your page can load and blocks the execution of unauthorized code. In the context of payment forms, CSP serves three purposes:
- Disables inline scripts and JavaScript: URL (unsafe-inline)
- Disables dynamic code generation via eval() (unsafe-eval)
- Limits script loading to trusted domains
How CSP works: The browser checks each script before execution. If a script doesn't comply with the policy, it is blocked before it's executed.
1.2. Strict CSP with nonces
The most effective CSP configuration for payment pages is to use nonces (one-time numbers) generated by the server for each request and inserted into the nonce attribute of each legitimate script and style. The nonce is unique for each page load and is useless to an attacker on the next request.An example CSP header:
Code:
Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com 'nonce-{RANDOM}'; style-src 'self' 'nonce-{RANDOM}';
1.3. Adyen Recommendations for Building a CSP
Adyen, one of the largest payment processors, offers a practical approach to CSP implementation:- Start with Content-Security-Policy-Report-Only, which logs violations but doesn't block resource loading. This allows you to identify all necessary sources without the risk of breaking the payment form.
- Enable the default-src 'self' directive to allow resources to be loaded only from your own domain.
- Add exceptions for the payment provider: script-src 'self' https://*.adyen.com.
- After validating all violations in reports, replace Report-Only with an active blocking policy.
When using payment providers, you need to add their domains to the CSP. For Trust Payments, for example, you need to allow the domain https://*.cardinaltrusted.com.
1.4. A New Threat in 2026: CSP Bypass via WebRTC
In March 2026, Sansec researchers discovered a new type of skimmer that uses WebRTC DataChannels to bypass CSPs. WebRTC connections, it turns out, are not regulated by standard CSP rules, allowing attackers to bypass protection even on hardened websites. Traffic is sent via encrypted DTLS packets over UDP, which are not inspected by most network security tools.This attack has been widely exploited since March 19, 2026, affecting more than half of vulnerable stores. The skimmer steals a valid CSP nonce from existing scripts to inject its payload, executing it during periods of browser inactivity to reduce the likelihood of detection.
Defenses against this vector include:
- Blocking unexpected outgoing UDP traffic at the network level
- Extending monitoring beyond HTTP inspection to cover encrypted UDP/DTLS streams
- Audit nonce generation and processing to prevent theft
1.5. Trusted Types API – A New Line of Defense Against DOM XSS
As of February 2026, the Trusted Types API is considered generally available (Baseline 2026) in all modern browsers. This API forces the developer to explicitly indicate that data has passed through a sanitization policy before passing it to dangerous sinks (innerHTML, eval, src, etc.).The developer defines a policy containing sanitization methods for different sink types. For example, a policy could use the DOMPurify library to sanitize HTML or completely prohibit the execution of dynamic code.
Trusted Types does not replace CSP, but rather complements it: CSP restricts script sources, and Trusted Types controls how these scripts manipulate the DOM. Best practice is to combine strict CSP without unsafe-inline with the Require-Trusted-Types-For 'script' header enabled.
1.6. Vulnerable implementations and their fixes
Example of a vulnerable implementation: The WordPress payment plugin for Stripe (version 1.4.6 or later) contained a stored XSS vulnerability (CVE-2026-0751). The fix involves escaping the output of all user data stored in the database before displaying it on the page.Example of unsafe code:
JavaScript:
// ❌ Dangerous: User data is written directly
document.getElementById('payment-message').innerHTML = userInput;
// ✅ Safe: Use textContent
document.getElementById('payment-message').textContent = userInput;
// ✅ Safe: Sanitize with DOMPurify before insertion
element.innerHTML = DOMPurify.sanitize(userInput);
CSP should be used in addition to screening and sanitization. No single method is a panacea — a combination of all three is necessary to provide multi-layered protection.
Part 2. Clickjacking Protection
Clickjacking is an attack in which a hacker embeds a victim's payment form in a transparent iframe on their website and tricks the user into clicking on the correct location, unaware that they are making a payment.2.1. X-Frame-Options и CSP frame-ancestors
Clickjacking protection is based on two mechanisms:- X-Frame-Options. An old, but still functional, header. The DENY value completely prevents the page from being embedded in any iframe; SAMEORIGIN allows embedding only from the same domain.
- CSP frame-ancestors. A more modern and flexible mechanism. This directive specifies which parent sources can embed a page via <frame>, <iframe>, <object>, or <embed>. CSP frame-ancestors is considered more powerful than X-Frame-Options because it supports a list of allowed domains and nested checks for all ancestors in a framework.
An example of reliable protection:
Code:
Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY
If the payment form is integrated via an iframe from subdomains of the same site:
Code:
Content-Security-Policy: frame-ancestors 'self' https://checkout.example.com;
2.2 Difference between frame-ancestors and frame-src
CSP distinguishes two types of embedding constraints:- frame-ancestors controls who can embed your page. This protects against clickjacking — who can embed your iframe on their website?
- frame-src controls which sources your page can load into its own iframes. This restricts where links on your page point.
2.3. Limitations and Recommendations
According to MDN documentation, the frame-ancestors directive is not supported in the <meta> element and must be specified via an HTTP header.Furthermore, there are differences in the handling of frame-ancestors and X-Frame-Options when using nested frames. If a page is embedded within multiple iframe levels, each ancestor must be resolved by the frame-ancestors directive of the final frame, otherwise the download will be canceled.
2.4. Vulnerable implementations and their fixes
Vulnerable implementation: The payment page doesn't set the X-Frame-Options and CSP frame-ancestors headers. The attacker embeds it in an invisible iframe on their site and intercepts clicks.
HTML:
<!-- Vulnerable: A hacker is embedding your form -->
<iframe src="https://your-shop.com/checkout" style="opacity:0; position:absolute;"></iframe>
Fix: Add HTTP headers on server (Nginx):
Code:
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none';" always;
Part 3. Protecting against Script Injection via Subresource Integrity (SRI)
Subresource Integrity (SRI) is a security mechanism that allows the browser to verify that a downloaded resource (such as a script from a CDN or a stylesheet) has not been modified by an attacker. The browser calculates a cryptographic hash of the downloaded file and compares it with the value specified in the integrity attribute.3.1. SRI Operating Principle
SRI protects against supply chain attacks, where a hacker compromises a CDN and replaces the library with a malicious one that intercepts payment data.Example use case:
HTML:
<script src="https://js.stripe.com/v3/"
integrity="sha384-..."
crossorigin="anonymous"></script>
If the loaded script differs from the specified hash, the browser refuses to execute it and reports an error in the console.
3.2. SRI and PCI DSS 6.4.3 Compliance
PCI DSS requirement 6.4.3 (mandatory since March 2025) requires organizations to ensure the integrity of scripts loaded on payment pages and clearly define which scripts should be executed, why they are required, and how to track their changes.Many payment providers provide official hash values for their scripts in their documentation. For example, Braintree, Adyen, and PaymentsOS recommend using SRI with versioned SDKs instead of automatically updating to the latest version.
3.3. Example for Braintree Hosted Fields
Braintree provides official hash values in its documentation. Using SRI with versioned scripts provides double protection: even if a hacker compromises the CDN and replaces a file, the browser will refuse to load it, and the payment will fail.3.4. An important difference between SRI and CSP
CSP determines where scripts can be loaded (source control). SRI verifies the exact script being loaded (content control). These mechanisms work together to create layered protection.3.5. Integration with PCI DSS: Requirements 6.4.3 and 11.6.1
It's important to understand that CSPs and SRIs are technical mechanisms, but they are not a complete solution for PCI DSS compliance. QSA auditors ask three key questions that CSPs cannot address:- What exactly is being executed on your payment page right now? (Not just the CSP policy, but the actual content in the browser)
- How did each script get onto the page and who approved it?
- How will you know if an authorized vendor changes their script tomorrow?
Therefore, in addition to CSP and SRI, the following are necessary:
- An inventory of all scripts on the payment page, indicating their purpose and source
- Documented approval of each script before adding
- Systems for continuous monitoring of script changes (for example, Feroot Security)
Part 4. Client-Side Tokenization: Removing Payment Data from Your Infrastructure
4.1. Architectural principle: Never touch the card data
The most effective way to protect payment data is to not process it at all. A key architectural principle: card numbers, CVVs, and expiration dates should never reach your server. All processing should occur client-side, in iframes or hosted fields owned by the payment provider.If payment data never reaches your server, your infrastructure is out-of-scope for PCI DSS, eliminating 80% of compliance requirements. A properly designed system has 5-10 PCI-compliant systems, while a poorly designed one has 50+, with comparable costs significantly higher.
4.2. Stripe Elements
Stripe Elements is a collection of pre-styled UI components in secure iframes that collect and tokenize payment data. Card data is passed directly to Stripe via the iframe; your server receives only a one-time token (PaymentMethod ID), which can be securely used for payments, refunds, and recurring charges.The standard workflow is: the client fills in the fields inside the iframe → Stripe tokenizes the data and returns a payment_method_id → your server receives the token → creates a PaymentIntent via the Stripe API with this token. Tokenized data means your systems never handle card numbers, mitigating risks and simplifying security compliance.
Benefits of Stripe Elements:
- Your server remains outside the PCI perimeter
- Card data never goes into logs, databases or caches
- Styling remains under your control
4.3. Braintree Hosted Fields
Braintree Hosted Fields implements a similar concept: each input field (card number, expiration date, CVV) is placed in its own transparent iframe, hosted on the Braintree (PayPal) domain. When the form is submitted, Braintree returns a one-time payment_method_nonce, which your server can securely use to process the transaction.Critical requirement: To comply with the simplified PCI DSS SAQ A level, payment fields cannot be directly located on your payment page. They must be hosted — placed in an iframe on the payment provider's domain.
4.4. Why tokenization doesn't solve all payment system security issues
Even when using Hosted Fields, the seller's area of responsibility includes:- Control which scripts are loaded on a page containing an iframe.
- Monitoring the integrity of these scripts (requirements 6.4.3 and 11.6.1 PCI DSS).
- Verifying the payment provider's webhook signature before processing notifications.
4.5. Example of a vulnerable implementation and its fix
Vulnerable implementation: Your server accepts the card number directly via POST, passes it to the Stripe API, and creates a PaymentIntent.
Code:
// ❌ Danger: The server processes the card number directly
app.post('/create-payment', (req, res) => {
const { cardNumber, expMonth, expYear, cvc } = req.body;
stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
payment_method_data: { type: 'card', card: { number: cardNumber, exp_month: expMonth, exp_year: expYear, cvc } }
});
});
Fix: Use Stripe Elements (or similar) for tokenization on the frontend and only pass payment_method_id to the server.
Part 5. End-to-end payment form security checklist
CSP and XSS protection:- Strict CSP without unsafe-inline and unsafe-eval. Use nonces or hashes for allowed scripts.
- The Content-Security-Policy header is included with a list of allowed origins, including payment provider domains.
- The frame-ancestors directive is set to 'none' or 'self' to prevent clickjacking.
- Trusted Types are enabled via the Require-Trusted-Types-For 'script' header in supported browsers.
- Implemented sanitization via DOMPurify for all dynamically inserted HTML fragments.
SRI and Script Integrity:
- The integrity attribute has been added to all CDN scripts (Stripe, Braintree, Adyen, analytics).
- Versioned links are used, not dynamic latest links.
- An inventory of all scripts on the payment page has been created and maintained, indicating their purpose, source, and approval date.
- A procedure has been developed for documented approval of each new script before it is added.
- Continuous monitoring of script changes with automatic alerts has been configured.
Tokenization and data protection:
- Payment data is collected through hosted fields (Stripe Elements, Braintree Hosted Fields, Adyen iframes), not in regular inputs.
- Only one-time tokens (payment_method_id, payment_nonce) are transmitted to the server, not card data.
- Payment provider webhooks are verified for signature authenticity (HMAC).
Clickjacking protection:
- The X-Frame-Options: DENY or SAMEORIGIN header is set on the server.
- The CSP frame-ancestors directive is configured to prevent external embedding.
- It has been verified that the page is not embedded on third-party domains (tested using developer tools).
Conclusion: Payment form security is not a single technology, but a layered defense.
A secure payment system isn't built on a single tool. It's a layered defense, with each layer covering the weaknesses of the others:- CSP + Trusted Types protect against script injection via XSS.
- SRI ensures that loaded scripts have not been tampered with at the CDN level.
- frame-ancestors + X-Frame-Options prevent clickjacking.
- Client-side tokenization completely removes payment data from your infrastructure, dramatically reducing your attack surface and simplifying PCI compliance.
The three main conclusions of this article are:
- CSP without unsafe-inline and unsafe-eval is mandatory. This is the only way to prevent unauthorized scripts from running on the payment page.
- SRI is mandatory for all CDN resources. Without it, a hacker who compromises your CDN can intercept all your transactions.
- Client-side tokenization is not an option, but a necessity. If card numbers pass through your server, you're exposed to high risk and a wide PCI perimeter.
A quick one-line reminder:
"Strict CSP without unsafe-inline, SRI for all CDNs, trust framing via X-Frame-Options and CSP frame-ancestors, hosted tokenization via Elements or Hosted Fields — and no card data on your server."
