How to enhance security of custom Stripe Payments using Stripe Elements for PCI DSS Compliance?

/blog/avatar-andrii-minchekov.png

July 4, 2024, written by

Andrii Minchekov

[object Object]

In a previous article, the benefits and use cases for the Stripe Payment Intent API flow were covered. Now, I dive deeper into enhancing this custom payment flow's security using Stripe Elements. Discover how to leverage Stripe's pre-built UI components to create a secure and seamless payment experience.

Stripe Elements Overview

How Stripe Elements Assure Security and PCI/DSS Compliance

  1. Usage of iframes for each secure field.

  2. Tokenization

    • Sensitive Data Handling: Stripe Elements securely handles card information and converts it into a token (a unique identifier). This token is used to process payments without exposing sensitive card details.
    • Client-Server Communication: The token is sent to your server, which then interacts with Stripe’s API to complete the payment. This ensures that sensitive data never touches your server.
  3. Secure Transmission

    • HTTPS: All communications between your client and Stripe are done over HTTPS, ensuring data is encrypted during transmission.
    • JS Encryption: Stripe’s JavaScript library encrypts card details before they are sent to Stripe’s servers.
  4. PCI Compliance

    • PCI SAQ A: By using Stripe Elements, you reduce your PCI DSS scope to SAQ A. This is the simplest PCI DSS Self-Assessment Questionnaire (SAQ) as it limits your exposure to sensitive card data.
    • Stripe’s Compliance: Stripe is a certified PCI Service Provider Level 1, the highest level of certification available in the payments' industry. By using Stripe Elements, you leverage Stripe’s infrastructure to handle PCI compliance requirements.
    • No Sensitive Data Storage: Since Stripe Elements ensures that card data never touches your servers, you avoid the complexities and risks associated with storing sensitive card information.
  5. Security Features

    • Built-in Validation: Stripe Elements includes built-in validation and formatting, reducing the risk of errors and improving security.
    • Dynamic Security Code: For cards that support it, Stripe Elements can dynamically display the card security code (CVC) field, adding an extra layer of security.

Tokenized Data Flow

Tokenization is the process Stripe uses to collect sensitive card or bank account details, or personally identifiable information (PII), directly from your customers in a secure manner. A token representing this information is returned to your server to use. Use Stripe recommended payments integrations to perform this process on the client-side. This guarantees that no sensitive card data touches your server, and allows your integration to operate in a PCI-compliant way.

If you can’t use client-side tokenization, you can also create tokens using the Tokens API with either your publishable or secret API key. If your integration uses this method, you’re responsible for any PCI compliance that it might require, and you must keep your secret API key safe. Unlike with client-side tokenization, your customer’s information isn’t sent directly to Stripe, so you are responsible to handle or store it securely.

Usage of iframes in Stripe Elements

An iframe (short for inline frame) is an HTML element that allows you to embed another HTML document within the current document. This embedded document can be isolated from the parent document, providing a layer of separation that enhances security and prevents tampering.

How Stripe Elements use iframes:

  1. Creation of Secure Input Fields:

    • When you use Stripe Elements to create input fields for card details, it embeds these fields within iframes. These iframes are hosted by Stripe's servers and not by your web server.
  2. Embedding iframes:

    • Each Stripe Element (e.g., card number, expiration date, CVC) is rendered inside an iframe. The iframe contains the HTML, CSS, and JavaScript necessary to securely collect card information.

How iframes are isolated to prevent tampering

  1. Cross-Origin Isolation:

    • The iframes created by Stripe Elements are served from a different origin (Stripe's domain) than your main webpage. This cross-origin setup provides security boundaries, preventing the parent page from accessing or manipulating the content of the iframe.
    • Browsers enforce the Same-Origin Policy, which restricts how documents or scripts loaded from one origin can interact with resources from another origin. This policy is a cornerstone of web security and helps to prevent attacks such as Cross-Site Scripting (XSS).
  2. Content Security Policy (CSP):

    • Stripe uses Content Security Policy (CSP) headers to further enhance security. CSP is a security feature that helps to detect and mitigate certain types of attacks, including XSS and data injection attacks. By specifying allowed sources for content, CSP restricts the ability of an attacker to inject malicious code.
    • When Stripe serves the iframe, it includes CSP headers to ensure that only approved scripts and styles can run within the iframe, adding an additional layer of protection.
  3. Isolation of Sensitive Data:

    • Since the card details are entered directly into the iframes served by Stripe, your webpage never has direct access to these details. This isolation means that even if your website is compromised, the attacker cannot access the card details entered into the Stripe Elements.
    • The iframe content is fully controlled and secured by Stripe, ensuring that the sensitive information is handled in a secure environment.
  4. JavaScript Security:

    • The JavaScript code that handles the encryption and tokenization of card details runs within the isolated iframe environment. This code is provided and controlled by Stripe, ensuring it adheres to best security practices.
    • The parent webpage cannot inject JavaScript into the iframe or interfere with the code running inside the iframe.

Example of using Stripe Elements to collect and tokenize card data

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Stripe Payment</title> <script src="https://js.stripe.com/v3/"></script> </head> <body> <form id="payment-form"> <div id="card-element"><!-- Stripe Elements will create input fields here --></div> <button id="submit">Pay</button> </form> <script> const stripe = Stripe('your-publishable-key'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element'); const form = document.getElementById('payment-form'); form.addEventListener('submit', async (event) => { event.preventDefault(); const { error, paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement, billing_details: { name: 'Cardholder Name', }, }); if (error) { console.error(error); } else { // Send the paymentMethod.id to your server const response = await fetch('/create-payment-intent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentMethodId: paymentMethod.id }), }); const { clientSecret } = await response.json(); // Confirm the payment on the client side const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment(clientSecret); if (confirmError) { console.error(confirmError); } else if (paymentIntent.status === 'succeeded') { console.log('Payment succeeded'); } } }); </script> </body> </html>

Key points of example:

This is how Stripe iframe code looks like after mounting to the DOM.

<div class="form-control StripeElement StripeElement--empty" id="cc_number"> <div class="__PrivateStripeElement" style="margin: 0px !important; padding: 0px !important; border: none !important; display: block !important; background: transparent !important; position: relative !important; opacity: 1 !important;"> <iframe name="__privateStripeFrame2365" src="https://js.stripe.com/v3/elements-inner-card-4c3fbb0b6f5096dd4a3a7a3ec37002fe.html#wait=true&amp;showIcon=true&amp;style[base][iconColor]=%23235fc6&amp;style[base][fontWeight]=500&amp;style[base][fontFamily]=Roboto%2C+sans-serif&amp;style[base][fontSize]=16px&amp;rtl=false&amp;componentName=cardNumber&amp;keyMode=test&amp;apiKey=pk_XXXXX&amp;referrer=https%3A%2F%2Fdevbenoit.bridgebase.com%2Fpurchase%2Fpay.php&amp;controllerId=__privateStripeController2361" title="Secure card number input frame" style="border: none !important; margin: 0px !important; padding: 0px !important; width: 1px !important; min-width: 100% !important; overflow: hidden !important; display: block !important; user-select: none !important; will-change: transform !important; height: 19.2px;"></iframe> <input class="__PrivateStripeElement-input" aria-hidden="true" aria-label=" " autocomplete="false" maxlength="1" style="border: none !important; display: block !important; position: absolute !important; height: 1px !important; top: -1px !important; left: 0px !important; padding: 0px !important; margin: 0px !important; width: 100% !important; opacity: 0 !important; background: transparent !important; pointer-events: none !important; font-size: 16px !important;"></div> </div>

Conclusion

Stripe Elements uses iframes to securely collect and tokenize card details, ensuring that these details are isolated from your webpage and protected from tampering. This approach leverages the Same-Origin Policy, Content Security Policy, and robust encryption methods to provide a secure environment for handling sensitive payment information, thereby enhancing security and simplifying compliance with PCI DSS requirements.

Stripe Integration
Stripe Elements
Payment Gateway
Secure Payments
PCI DSS
Fintech