100,000 WordPress Sites Affected by Arbitrary File Upload Vulnerability in AI Engine WordPress Plugin


📢 Calling all Vulnerability Researchers and Bug Bounty Hunters! 📢

🌞 Spring into Summer with Wordfence! Now through August 4, 2025, earn 2X bounty rewards for all in-scope submissions from our ‘High Threat’ list in software with fewer than 5 million active installs. Bounties up to $31,200 per vulnerability. Submit bold. Earn big!


On July 18th, 2025, we received a submission for an Arbitrary File Upload vulnerability in AI Engine, a WordPress plugin with more than 100,000 active installations. This vulnerability can be used by authenticated attackers, with subscriber-level access and above, to upload arbitrary files to a vulnerable site and achieve remote code execution, which is typically leveraged for a complete site takeover. Please note that this vulnerability only critically affects users who have enabled the “Public API” option in the settings, which is disabled by default, and have not configured authentication for the API.

Props to ISMAILSHADOW who discovered and responsibly reported this vulnerability through the Wordfence Bug Bounty Program. This vulnerability was disclosed to our program just one day after it was introduced. This researcher earned a bounty of $1,170.00 for this discovery. Our mission is to secure WordPress through defense in depth, which is why we are investing in quality vulnerability research and collaborating with researchers of this caliber through our Bug Bounty Program. We are committed to making the WordPress ecosystem more secure through the detection and prevention of vulnerabilities, which is a critical element to our multi-layered approach to security.

Wordfence Premium, Wordfence Care, and Wordfence Response users received a firewall rule to protect against any exploits targeting this vulnerability on July 21, 2025. Sites using the free version of Wordfence will receive the same protection 30 days later on August 20, 2025.

We provided full disclosure details to Jordy Meow instantly through our Wordfence Vulnerability Management Portal on July 18, 2025. The developer released the patch on July 22, 2025. We would like to commend Jordy Meow for their prompt response and timely patch.

This vulnerability is an incredible example of the positive impact the Wordfence Bug Bounty Program has on the WordPress ecosystem. The vulnerability was introduced on July 17th, 2025, and just one day later was reported to our Bug Bounty Program, triaged, sent to the developer, and patched a few days later, creating a narrow opportunity for attackers to discover and exploit this vulnerability before site owners were protected. Extra special kudos to the researcher ISMAILSHADOW for discovering this vulnerability so quickly and to Jordy Meow for remediating the vulnerability so quickly.

We urge users to update their sites with the latest patched version of AI Engine, version 2.9.5 at the time of this publication, as soon as possible.

Vulnerability Summary from Wordfence Intelligence

CVSS Rating
8.8 (High)
Affected Versions
2.9.3 – 2.9.4
Patched Version
2.9.5
Bounty
$1,170.00
Affected Software
Affected Software Slug
Researcher

The AI Engine plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the rest_simpleFileUpload() function in versions 2.9.3 and 2.9.4. This makes it possible for authenticated attackers, with Subscriber-level access and above, to upload arbitrary files on the affected site’s server when the REST API is enabled, which may make remote code execution possible.

Technical Analysis

AI Engine recently introduced a simple file upload for the Public API.

Examining the code reveals that the plugin uses the can_access_public_api() function in the Meow_MWAI_Core class to perform the permission check to the Public REST API endpoint. Initially, access is granted to all logged-in users. However, the plugin supports Bearer Token authentication. When the token is configured in the settings, the auth_via_bearer_token() function is integrated into the 'mwai_allow_mcp' filter for authentication checks. Additionally, the 'mwai_allow_mcp' filter is used to add custom authentication methods. This means that on sites that do not have an authentication mechanism specified, the REST API endpoints are accessible to authenticated users.

register_rest_route( 'mwai/v1', '/simpleFileUpload', [
  'methods' => 'POST',
  'callback' => [ $this, 'rest_simpleFileUpload' ],
  'permission_callback' => function ( $request ) {
    return $this->core->can_access_public_api( 'simpleFileUpload', $request );
  },
] );
public function can_access_public_api( $feature, $extra ) {
  $logged_in = is_user_logged_in();
  return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
}
public function rest_api_init() {
  $public_api = $this->core->get_option( 'public_api' );
  if ( !$public_api ) {
    return;
  }
  $this->bearer_token = $this->core->get_option( 'public_api_bearer_token' );
  if ( !empty( $this->bearer_token ) ) {
    add_filter( 'mwai_allow_public_api', [ $this, 'auth_via_bearer_token' ], 10, 3 );
  }

Further examination of the code reveals that the plugin uses the simpleFileUpload() function in the Meow_MWAI_API class to handle the file uploads via the REST API. This function calls the upload_file() function in the Meow_MWAI_Modules_Files class, which uses the copy() function to save the uploaded file to the uploads folder.

public function upload_file(
  $path,
  $filename = null,
  $purpose = null,
  $metadata = null,
  $envId = null,
  $target = null,
  $expiry = null
) {
  require_once( ABSPATH . 'wp-admin/includes/image.php' );
  require_once( ABSPATH . 'wp-admin/includes/file.php' );
  require_once( ABSPATH . 'wp-admin/includes/media.php' );

  $target = empty( $target ) ? $this->core->get_option( 'image_local_upload' ) : $target;
  $expiry = empty( $expiry ) ? $this->core->get_option( 'image_expires' ) : $expiry;

  $expires = ( $expiry === 'never' || empty( $expiry ) ) ? null : date( 'Y-m-d H:i:s', time() + intval( $expiry ) );
  $refId = $this->generate_refId();
  $url = null;
  if ( empty( $filename ) ) {
    $parsed_url = parse_url( $path, PHP_URL_PATH );
    $filename = basename( $parsed_url );
    $extension = pathinfo( $filename, PATHINFO_EXTENSION );
  }
  else {
    $extension = pathinfo( $filename, PATHINFO_EXTENSION );
  }
  $newFilename = $refId . '.' . $extension;
  $unique_filename = wp_unique_filename( wp_upload_dir()['path'], $newFilename );
  $destination = wp_upload_dir()['path'] . '/' . $unique_filename;

  if ( $target === 'uploads' ) {
    if ( !$this->check_db() ) {
      throw new Exception( 'Could not create database table.' );
    }
    if ( !copy( $path, $destination ) ) {

Unfortunately, these upload functions do not include any file type or extension checks in the vulnerable version. This means that not only image, video or document files can be uploaded, but it is also possible to upload files with a .php extension. The file is uploaded to the WordPress uploads folder, which is publicly accessible by default. This makes it possible for authenticated attackers to upload arbitrary malicious PHP code and then access the file to trigger remote code execution on the server.

As with all arbitrary file upload vulnerabilities, this can lead to complete site compromise through the use of webshells and other techniques.

Please note that this vulnerability can only be exploited if the “Public API” option is enabled, which is disabled by default, and no Bearer Token is configured, nor is custom authentication added and used to protect the API. The primary purpose of this Public REST API is to enable integration with automation platforms and external applications, so it should only be accessible after proper authentication.

The Patch

The vendor patched this issue by adding a file type check using the wp_check_filetype() function to the simpleFileUpload() function in the Meow_MWAI_API class, and also adding the same file type check to the upload_file() function in the Meow_MWAI_Modules_Files class. This ensures that only safe file types can be uploaded.

public function simpleFileUpload( $file = null, $base64 = null, $filename = null, $purpose = 'files', $ttl = 3600, $target = null, $metadata = [] ) {
  global $mwai_core;

  if ( !$this->core->files ) {
    throw new Exception( 'Files module is not available.' );
  }

  // Determine target from settings if not provided
  if ( empty( $target ) ) {
    $target = $this->core->get_option( 'image_local_upload', 'uploads' );
  }

  try {
    if ( !empty( $base64 ) ) {
      // Handle base64 upload
      if ( empty( $filename ) ) {
        $filename = 'upload-' . time() . '.dat';
      }

      // Validate filename extension for base64 uploads
      $validate = wp_check_filetype( $filename );
      if ( $validate['type'] == false ) {
        throw new Exception( 'File type is not allowed.' );
      }

      // For base64 uploads, we need to decode and create a temp file first
      $binary = base64_decode( $base64 );
      if ( !$binary ) {
        throw new Exception( 'Invalid base64 data.' );
      }

      // Create a temporary file
      $tmp_path = wp_tempnam( 'mwai-upload' );
      file_put_contents( $tmp_path, $binary );

      // Use the regular upload method
      $refId = $this->core->files->upload_file(
        $tmp_path,
        $filename,
        $purpose,
        $metadata,
        null, // envId
        $target,
        $ttl
      );
public function upload_file(
  $path,
  $filename = null,
  $purpose = null,
  $metadata = null,
  $envId = null,
  $target = null,
  $expiry = null
) {
  require_once( ABSPATH . 'wp-admin/includes/image.php' );
  require_once( ABSPATH . 'wp-admin/includes/file.php' );
  require_once( ABSPATH . 'wp-admin/includes/media.php' );

  $target = empty( $target ) ? $this->core->get_option( 'image_local_upload' ) : $target;
  $expiry = empty( $expiry ) ? $this->core->get_option( 'image_expires' ) : $expiry;

  $expires = ( $expiry === 'never' || empty( $expiry ) ) ? null : date( 'Y-m-d H:i:s', time() + intval( $expiry ) );
  $refId = $this->generate_refId();
  $url = null;
  if ( empty( $filename ) ) {
    $parsed_url = parse_url( $path, PHP_URL_PATH );
    $filename = basename( $parsed_url );
    $extension = pathinfo( $filename, PATHINFO_EXTENSION );
  }
  else {
    $extension = pathinfo( $filename, PATHINFO_EXTENSION );
  }

  // Validate file type using WordPress built-in function
  $validate = wp_check_filetype( $filename );
  if ( $validate['type'] == false ) {
    throw new Exception( 'File type is not allowed.' );
  }
  $newFilename = $refId . '.' . $extension;
  $unique_filename = wp_unique_filename( wp_upload_dir()['path'], $newFilename );
  $destination = wp_upload_dir()['path'] . '/' . $unique_filename;

  if ( $target === 'uploads' ) {
    if ( !$this->check_db() ) {
      throw new Exception( 'Could not create database table.' );
    }
    if ( !copy( $path, $destination ) ) {

Disclosure Timeline

July 18, 2025 – We received the submission for the Arbitrary File Upload vulnerability in AI Engine via the Wordfence Bug Bounty Program.
July 18, 2025 – We validated the report and confirmed the proof-of-concept exploit.
July 18, 2025 – Full disclosure details were sent instantly to the vendor through our Wordfence Vulnerability Management Portal.
July 21, 2025Wordfence Premium, Care, and Response users received a firewall rule to provide added protection against any exploits that may target this vulnerability.
July 22, 2025 – The vendor acknowledged the report and began working on a fix.
July 22, 2025 – The fully patched version of the plugin, 2.9.5, was released.
August 20, 2025 – Wordfence Free users will receive the same protection.

Conclusion

In this blog post, we detailed an Arbitrary File Upload vulnerability within the AI Engine plugin affecting versions 2.9.3 through 2.9.4. This vulnerability allows authenticated threat actors with subscriber-level permissions or higher to execute malicious code on the server. The vulnerability has been fully addressed in version 2.9.5 of the plugin.

We encourage WordPress users to verify that their sites are updated to the latest patched version of AI Engine as soon as possible considering the critical nature of this vulnerability.

Wordfence Premium, Wordfence Care, and Wordfence Response users received a firewall rule to protect against any exploits targeting this vulnerability on July 21, 2025. Sites using the free version of Wordfence will receive the same protection 30 days later on August 20, 2025.

If you know someone who uses this plugin on their site, we recommend sharing this advisory with them to ensure their site remains secure, as this vulnerability poses a significant risk.

The post 100,000 WordPress Sites Affected by Arbitrary File Upload Vulnerability in AI Engine WordPress Plugin appeared first on Wordfence.

Leave a Comment