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
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.