Unauthenticated IDOR to PII Disclosure in WooCommerce Stripe Gateway Plugin

Published 13 June 2023
Updated 24 July 2023
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

This blog post is about the WooCommerce Stripe Gateway plugin vulnerability. If you're a WooCommerce Stripe Gateway user, please update the plugin to at least version 7.4.1.

Patchstack paid plan users are protected from the vulnerability. You can also sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.

For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.

About the WooCommerce Stripe Gateway WordPress plugin

The plugin WooCommerce Stripe Gateway (versions 7.4.0 and below, free version), which has over 900,000 active installations is known as the most popular WooCommerce Stripe payment plugin in WordPress. This plugin is developed by WooCommerce.

This plugin is a WordPress plugin which allows you to accept payments directly on a store for web and mobile. With the plugin, customers can stay on the store during checkout instead of being redirected to an externally hosted checkout page.

The security vulnerability in WooCommerce Stripe Gateway

This plugin suffers from an Unauthenticated Insecure Direct Object Reference (IDOR) vulnerability. This vulnerability allows any unauthenticated user to view any WooCommnerce order's PII data including email, user's name, and full address. The described vulnerability was fixed in version 7.4.1 with some backported fixed version and assigned CVE-2023-34000.

Find out more from the Patchstack database.

The first underlying vulnerability is located on javascript_params function :

public function javascript_params() {
    global $wp;

    $order_id = absint( get_query_var( 'order-pay' ) );

    $stripe_params = [
        'title'                    => $this->title,
        'key'                      => $this->publishable_key,
        'i18n_terms'               => __( 'Please accept the terms and conditions first', 'woocommerce-gateway-stripe' ),
        'i18n_required_fields'     => __( 'Please fill in required checkout fields first', 'woocommerce-gateway-stripe' ),
        'updateFailedOrderNonce'   => wp_create_nonce( 'wc_stripe_update_failed_order_nonce' ),
        'updatePaymentIntentNonce' => wp_create_nonce( 'wc_stripe_update_payment_intent_nonce' ),
        'orderId'                  => $order_id,
        'checkout_url'             => WC_AJAX::get_endpoint( 'checkout' ),
    ];

    // If we're on the pay page we need to pass stripe.js the address of the order.
    if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // wpcs: csrf ok.
        $order_id = wc_clean( $wp->query_vars['order-pay'] ); // wpcs: csrf ok, sanitization ok, xss ok.
        $order    = wc_get_order( $order_id );

        if ( is_a( $order, 'WC_Order' ) ) {
            $stripe_params['billing_first_name'] = $order->get_billing_first_name();
            $stripe_params['billing_last_name']  = $order->get_billing_last_name();
            $stripe_params['billing_address_1']  = $order->get_billing_address_1();
            $stripe_params['billing_address_2']  = $order->get_billing_address_2();
            $stripe_params['billing_state']      = $order->get_billing_state();
            $stripe_params['billing_city']       = $order->get_billing_city();
            $stripe_params['billing_postcode']   = $order->get_billing_postcode();
            $stripe_params['billing_country']    = $order->get_billing_country();
        }
    }
-----------------------------------------------------------------------------------------

Notice that the code will fetch an order object to $order variable using the $order_id variable. The $order_id variable is constructed from $wp->query_vars['order-pay']. According to the query_vars documentation, this hook could be used to fetch parameter from the GET parameters.

The code then will construct a $stripe_params variable with details from the $order object such as user's full name and full address. There is no orders ownership check on the rest of the function code and the function will return $order as an object.

When traced, the javascript_params variable could be called from the payment_scripts function:

public function payment_scripts() {
-----------------------------------------------------------------------------------------
wp_localize_script(
    'woocommerce_stripe',
    'wc_stripe_params',
    apply_filters( 'wc_stripe_params', $this->javascript_params() )
);
-----------------------------------------------------------------------------------------

The wp_localize_script function will return a JavaScript object variable to the front-end. The overall function call could be triggered from the site's front page and the order's PII disclosure will be reflected back into the page source.

The second vulnerable code exist in the payment_fields function:

public function payment_fields() {
    global $wp;
    $user                 = wp_get_current_user();
    $display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && $this->saved_cards;
    $total                = WC()->cart->total;
    $user_email           = '';
    $description          = $this->get_description();
    $description          = ! empty( $description ) ? $description : '';
    $firstname            = '';
    $lastname             = '';

    // If paying from order, we need to get total from order not cart.
    if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) { // wpcs: csrf ok.
        $order      = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) ); // wpcs: csrf ok, sanitization ok.
        $total      = $order->get_total();
        $user_email = $order->get_billing_email();
    } else {
        if ( $user->ID ) {
            $user_email = get_user_meta( $user->ID, 'billing_email', true );
            $user_email = $user_email ? $user_email : $user->user_email;
        }
    }

    if ( is_add_payment_method_page() ) {
        $firstname = $user->user_firstname;
        $lastname  = $user->user_lastname;
    }

    ob_start();

    echo '<div
        id="stripe-payment-data"
        data-email="' . esc_attr( $user_email ) . '"
        data-full-name="' . esc_attr( $firstname . ' ' . $lastname ) . '"
        data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
    >';

The condition is the same as the first vulnerable code, there is no orders ownership check on the code and the function will output the billing email and user's full name to the front-end.

The patch in WooCommerce Stripe Gateway

Since the issue is mainly because of the code not validating the fetched order ownership, checking the endpoint using the is_valid_pay_for_order_endpoint function which will check the order based on the key and ownership should fix the issue. The two patches could be seen here and here:

Conclusion

One of the critical flow for WooCommerce related plugin is handling order object. In many cases, order object is referenced from user input that is coming from WordPress query_vars. Make sure to always check access control around order object by checking the order key and ownership.

Timeline

17 April 2023We found the vulnerability and reached out to the plugin vendor.
30 May 2023WooCommerce Stripe Gateway version 7.4.1 was published to patch the reported issues.
13 June 2023Added the vulnerabilities to the Patchstack vulnerability database.
13 June 2023Published the article.

Help us make the internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you're a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
  • If you're a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu