This blog post is about an AI Engine plugin vulnerability. If you’re an AI Engine user, please update the plugin to at least version 1.9.99.
You can 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 AI Engine Plugin
The plugin AI Engine (free version), which has over 50,000 active installations, is known as the more popular AI-related plugin in WordPress.
This plugin enables us to create chatbot, craft content, coordinate AI-related work using templates, play with AI Copilot in the editor for faster work, track statistics, and more. The AI Playground offers a range of AI tools, including translation, correction, SEO, suggestions, WooCommerce product fields, and others. There is also an internal API so other plugins can tap into its capabilities.
The security vulnerability
This plugin suffers from an unauthenticated arbitrary file upload vulnerability. This vulnerability allows any unauthenticated user to upload arbitrary files, including php files, that could lead to remote code execution. The described vulnerability was fixed in version 1.9.99 and assigned CVE-2023-51409.
Unauthenticated Arbitrary File Upload
The underlying vulnerable code exists in the rest_upload
function:
public function rest_upload() {
require_once( ABSPATH . 'wp-admin/includes/image.php' );
require_once( ABSPATH . 'wp-admin/includes/file.php' );
require_once( ABSPATH . 'wp-admin/includes/media.php' );
$file = $_FILES['file'];
$error = null;
if ( empty( $file ) ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'No file provided.' ], 400 );
}
$local_upload = $this->core->get_option( 'image_local_upload' );
$image_expires_seconds = $this->core->get_option( 'image_expires' );
$expires = ( empty( $image_expires_seconds ) || $image_expires_seconds === 'never' ) ? null :
date( 'Y-m-d H:i:s', time() + $image_expires_seconds );
$fileId = null;
$url = null;
if ( $local_upload === 'uploads' ) {
if ( !$this->check_db() ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'Could not create database table.' ], 500 );
}
$upload_dir = wp_upload_dir();
$filename = wp_unique_filename( $upload_dir['path'], $file['name'] );
$path = $upload_dir['path'] . '/' . $filename;
if ( !move_uploaded_file( $file['tmp_name'], $path ) ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'Could not move the file.' ], 500 );
}
$url = $upload_dir['url'] . '/' . $filename;
$fileId = md5( $url );
$this->wpdb->insert( $this->table_files, [
'fileId' => $fileId,
'type' => 'image',
'status' => 'uploaded',
'created' => date( 'Y-m-d H:i:s' ),
'updated' => date( 'Y-m-d H:i:s' ),
'expires' => $expires,
'path' => $path,
'url' => $url
]);
}
else if ( $local_upload === 'library' ) {
$id = media_handle_upload( 'file', 0 );
if ( is_wp_error( $id ) ) {
$error = $id->get_error_message();
return new WP_REST_Response([ 'success' => false, 'message' => $error ], 500);
}
$url = wp_get_attachment_url( $id );
$fileId = md5( $url );
update_post_meta( $id, '_mwai_file_id', $fileId );
update_post_meta( $id, '_mwai_file_expires', $expires );
}
return new WP_REST_Response( [
'success' => true,
'data' => [ 'id' => $fileId, 'url' => $url ]
], 200 );
}
This function handles requests to the mwai-ui/v1/files/upload
REST API endpoint:
public function rest_api_init() {
register_rest_route( $this->namespace, '/files/upload', array(
'methods' => 'POST',
'callback' => array( $this, 'rest_upload' ),
'permission_callback' => '__return_true'
) );
register_rest_route( $this->namespace, '/files/delete', array(
'methods' => 'POST',
'callback' => array( $this, 'rest_delete' ),
'permission_callback' => '__return_true'
) );
}
Note that the permission_callback
parameter of the REST endpoint is set to __return_true
which allows any unauthenticated user to trigger the rest_upload
function.
Back to the rest_upload
function, notice that in one of the conditions, the code will try to save the uploaded files with $path
value using the move_uploaded_file
function. The $path
itself is constructed from $filename
variable and it contains the $file['name']
value which we have full control over. Since there is no proper file type and extension validation on the function, we can simply upload an arbitrary file such as .php file to achieve RCE.
Note that this vulnerability can be reproduced from by an unauthenticated user on a default installation of the plugin without any additional conditions or requirements.
The patch
The team decided to apply a permission check to the custom REST API endpoint and also apply a check on the file type and extension using the wp_check_filetype_and_ext
function. The patch can be seen below:
Conclusion
Always check every process of $_FILES parameter in the plugin or theme code. Make sure to apply a check on the filename and extension before uploading the file. Also, pay extra attention to the permission checks on the custom REST API 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.