XSS Vulnerability in LiteSpeed Cache Plugin Affecting 4+ Million Sites

Published 27 February 2024
Updated 28 February 2024
Table of Contents

This blog post is about the LiteSpeed plugin vulnerability. If you’re a LiteSpeed user, please update the plugin to at least version 5.7.0.1.

All paid Patchstack users are protected from this vulnerability. Sign up for the free Community account first, to scan for vulnerabilities and apply protection for only $5 / site per month with Patchstack.

For plugin developers, we have security audit services and Enterprise API for hosting companies.

About the LiteSpeed Cache Plugin

The plugin LiteSpeed Cache (free version), which has over 4 million active installations, is known as the most popular caching plugin in WordPress.

This WordPress plugin is an all-in-one site acceleration plugin, featuring an exclusive server-level cache and a collection of optimization features. The plugin supports WordPress Multisite and is compatible with the most popular plugins, including WooCommerce, bbPress, and Yoast SEO.

The security vulnerability

This plugin suffers from unauthenticated site-wide stored XSS vulnerability and could allow any unauthenticated user from stealing sensitive information to, in this case, privilege escalation on the WordPress site by performing a single HTTP request.

This vulnerability occurs because the code that handles input from the user doesn’t implement sanitization and output escaping. This case also combined with improper access control on one of the available REST API endpoints from the plugin. The described vulnerability was fixed in version 5.7.0.1 and assigned CVE-2023-40000.

The main vulnerability exists in the function update_cdn_status:

/**
 * Callback for updating Auto CDN Setup status after run
 *
 * @since  4.7
 * @access public
 */
public function update_cdn_status() {

    if ( !isset( $_POST[ 'success' ] ) || !isset( $_POST[ 'result' ] ) ) {
        self::save_summary( array( 'cdn_setup_err' => __( 'Received invalid message from the cloud server. Please submit a ticket.', 'litespeed-cache' ) ) );
        return self::err( 'lack_of_param' );
    }
    if (!$_POST[ 'success' ]) {
        self::save_summary( array( 'cdn_setup_err' => $_POST[ 'result' ][ '_msg' ] ) );
        Admin_Display::error( __( 'There was an error during CDN setup: ', 'litespeed-cache' ) . $_POST[ 'result' ][ '_msg' ] );
    } else {
        $this->_process_cdn_status($_POST[ 'result' ]);
    }

    return self::ok();
}

This function is called from cdn_status function:

/**
 * Endpoint for QC to notify plugin of CDN setup status update.
 *
 * @since  3.0
 */
public function cdn_status() {
    return $this->cls( 'Cdn_Setup' )->update_cdn_status();
}

The cdn_status itself is confirmed as a function handler for litespeed/v1/cdn_status REST API endpoint :

// CDN setup callback notification
register_rest_route( 'litespeed/v1', '/cdn_status', array(
    'methods' => 'POST',
    'callback' => array( $this, 'cdn_status' ),
    'permission_callback'	=> array( $this, 'is_from_cloud' ),
) );

The endpoint is protected by is_from_cloud which is set as the permission_callback argument that should check the user permission that accessed the specified endpoint. Turns out that this function only returns true which would allow any unauthenticated user to access the endpoint :

/**
 * Check if the request is from cloud nodes
 *
 * @since 4.2
 * @since 4.4.7 As there is always token/api key validation, ip validation is redundant
 */
public function is_from_cloud() {
    return true;
    // return $this->cls( 'Cloud' )->is_from_cloud();
}

Back to the update_cdn_status function, one of the condition sets could make the function trigger Admin_Display::error() function with unsanitized $_POST[ 'result' ][ '_msg' ] parameter supplied as the input parameter.

The Admin_Display::error() function itself is just a wrapper function to display an admin notice, which in WordPress context is a message to display information, alerts, and messages to users inside of the wp-admin area.

Function update_cdn_status could also trigger a call to _process_cdn_status function :

/**
 * Process the returned Auto CDN Setup status
 *
 * @since  4.7
 * @access private
 */
private function _process_cdn_status($result) {

    if ( isset($result[ 'nameservers' ] ) ) {
        if (isset($this->_summary['cdn_setup_err'])) {
            unset($this->_summary['cdn_setup_err']);
        }
        if (isset($result[ 'summary' ])) {
            $this->_summary[ 'cdn_dns_summary' ] = $result[ 'summary' ];
        }
        $this->cls( 'Cloud' )->set_linked();
        $this->cls( 'Conf' )->update_confs( array( self::O_QC_NAMESERVERS => $result[ 'nameservers' ], self::O_CDN_QUIC => true ) );
        Admin_Display::succeed( '🎊 ' . __( 'Congratulations, QUIC.cloud successfully set this domain up for the CDN. Please update your nameservers to:', 'litespeed-cache' ) . $result[ 'nameservers' ] );
    } else if ( isset($result[ 'done' ] ) ) {
        if ( isset( $this->_summary[ 'cdn_setup_err' ] ) ) {
            unset( $this->_summary[ 'cdn_setup_err' ] );
        }
        if ( isset( $this->_summary[ 'cdn_verify_msg' ] ) ) {
            unset( $this->_summary[ 'cdn_verify_msg' ] );
        }
        $this->_summary[ 'cdn_setup_done_ts' ] = time();

        $this->_setup_token = '';
        $this->cls( 'Conf' )->update_confs( array( self::O_QC_TOKEN => '', self::O_QC_NAMESERVERS => '' ) );
    } else if ( isset($result[ '_msg' ] ) ) {
        $notice = $result[ '_msg' ];
        if ( $this->conf( Base::O_QC_NAMESERVERS )) {
            $this->_summary[ 'cdn_verify_msg' ] = $result[ '_msg' ];
            $notice = array('cdn_verify_msg' => $result[ '_msg' ]);
        }
        Admin_Display::succeed( $notice );
    } else {
        Admin_Display::succeed( __( 'CDN Setup is running.', 'litespeed-cache' ) );
    }
    self::save_summary();
}

The $result the variable itself is an unsanitized value of $_POST[ 'result' ] parameter. Similar to the condition in update_cdn_status, it utilizes Admin_Display::succeed() function which is the same process as Admin_Display::error() function and only differs on the type of success or error message.

Note that this vulnerability is reproducible in a default installation and activation of the LiteSpeed Cache plugin without a specific requirement or configuration. Since the XSS payload is placed as an admin notice and the admin notice could be displayed on any wp-admin endpoint, this vulnerability also could be easily triggered by any user that has access to the wp-admin area.

The patch

Since this vulnerability exists because the code constructs an HTML value directly from the POST body parameter to the admin notice message, sanitizing user input using esc_html directly on the affected parameter should be enough to fix the issue. Additionally, the vendor also added a permission check on the update_cdn_status function via hash validation to limit the access to the function to only privileged users. The patch can be seen below:

Conclusion

We recommend applying escaping and sanitization to any message that will be displayed as an admin notice. Depending on the context of the data, we recommend using sanitize_text_field to sanitize value for HTML output (outside of HTML attribute) or esc_html. For escaping values inside of attributes, you can use the esc_attr function. We also recommend applying a proper permission or authorization check to the registered rest route endpoints.

Timeline

17 October, 2023We found the vulnerability and reached out to the plugin vendor. Deployed vPatch rule to protect users.
25 October, 2023LiteSpeed Cache plugin version 5.7.0.1 released to patch the reported issues.
27 February, 2024Added the vulnerabilities to the Patchstack vulnerability database. Security advisory article publicly released.

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