200,000 WordPress Sites Affected by Arbitrary File Deletion Vulnerability in Perfmatters WordPress Plugin

On March 1st, 2026, we received a submission for an Arbitrary File Deletion vulnerability in Perfmatters, a WordPress plugin with more than 200,000 active installations. This vulnerability makes it possible for unauthenticated threat actors to delete arbitrary files, including the wp-config.php file, which can make site takeover and remote code execution possible.

Props to hoshino who discovered and responsibly reported this vulnerability through the Wordfence Bug Bounty Program. This researcher earned a bounty of $3,726.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 the multi-layered approach to security.

All Wordfence Premium, Wordfence Care, and Wordfence Response customers, as well as those using the free version of our plugin, are protected against any exploits targeting this vulnerability by the Wordfence firewall’s built-in Local File Inclusion protection.

We contacted the forgemedia LLC team on March 17, 2026, and they registered on our Wordfence Vulnerability Management Portal for WordPress vendors on the same day. After the vendor registered and verified ownership of their software through the Wordfence Vendor Portal, the full disclosure details were sent immediately to the vendor. The developer released the patch on March 25, 2026. We would like to commend the forgemedia LLC team for their prompt response and timely patch.

We urge users to update their sites with the latest patched version of Perfmatters, version 2.6.0 at the time of this writing, as soon as possible.


🔥🔥🔥 Triple Threat Bug Bounty Challenge 🔥🔥🔥
Hunt High Threat vulnerabilities and earn triple the incentives!

Now through April 6, 2026, earn three stacked bonuses on all valid submissions from our ‘High Threat Vulnerabilities’ list:

  • 💰 2x all high threat vulnerability bounties (excluding 5,000,000+ installs)
  • 📈 +30% bonus for high threat vulnerabilities in software with 30,000+ active installs (excluding 5,000,000+ installs)
  • 🎯 $300 extra for every 3 High Threat vulnerabilities submitted (minimum of 1,000 installs)

Use the Bounty Estimator to see what rewards are possible through the promotion.

Submit through our Bug Bounty Program today to maximize your impact and your payout.


Vulnerability Summary from Wordfence Intelligence

CVSS Rating
8.1 (High)
Affected Version(s)
<= 2.5.9.1
Patched Version
2.6.0
Bounty
$3,726.00
Affected Software
Perfmatters [perfmatters]
Researcher
The Perfmatters plugin for WordPress is vulnerable to arbitrary file deletion via path traversal in all versions up to, and including, 2.5.9.1. This is due to the `PMCS::action_handler()` method processing the `$_GET[‘delete’]` parameter without any sanitization, authorization check, or nonce verification. The unsanitized filename is concatenated with the storage directory path and passed to `unlink()`. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete arbitrary files on the server by using `../` path traversal sequences, including `wp-config.php` which would force WordPress into the installation wizard and allow full site takeover.

Technical Analysis

Examining the code reveals that the plugin uses the action_handler() function in the PMCS class to manage actions related to snippets.

public static function action_handler()
{
    global $pmcs_error;

    if(!empty($_GET['page']) && $_GET['page'] == 'perfmatters') {

        //bulk actions
        if(!empty($_GET['action2']) && !empty($_GET['snippets'])) {

            switch($_GET['action2']) {

                case 'deactivate':

                    foreach($_GET['snippets'] as $snippet) {
                        Snippet::deactivate($snippet);
                    }

                    //success message
                    self::admin_notice_redirect('action2', 'deactivated');

                    break;

                case 'activate':

                    $config = self::get_snippet_config();

                    foreach($_GET['snippets'] as $snippet) {
                        if(!isset($config['error_files'][$snippet])) {
                            Snippet::activate($snippet);
                        }
                    }

                    //success message
                    self::admin_notice_redirect('action2', 'activated');

                    break;

                case 'export':

                    self::export($_GET['snippets']);

                    break;

                case 'delete':

                    Snippet::delete($_GET['snippets']);

                    //success message
                    self::admin_notice_redirect('action2', 'deleted');

                    break;
            }
        }

        //export snippet
        if(!empty($_GET['export'])) {
            self::export($_GET['export']);
        }

        //delete snippet
        if(!empty($_GET['delete'])) {

            //vars
            $file_name = $_GET['delete'];
            $file = self::get_storage_dir() . '/' . $file_name;

            //file not found
            if(!is_file($file) || $file_name === 'index.php') {
                self::admin_notice_redirect('delete', 'file_not_found');
            }

            //delete
            Snippet::delete($_GET['delete']);

Unfortunately, it was found that capability checks as well as nonce checks were missing in this function in the vulnerable versions. This makes it possible for unauthenticated attackers to invoke the delete snippet action.

//delete snippet, supports multiple
public static function delete($file_names)
{
	//get config
    $config = PMCS::get_snippet_config();

    foreach((array)$file_names as $file_name) {

        //get snippet data
        $snippet = self::get($file_name);

        //delete cached files if needed
        if(!empty($snippet['meta']['type']) && in_array($snippet['meta']['type'], ['js', 'css'])) {
            $cached_files = PMCS::get_storage_dir() . '/' . $snippet['meta']['type'] . '/' . str_replace('.php', '*', $file_name);
            foreach(glob($cached_files) as $cached_file) {
			    unlink($cached_file);
			}
        }

        //delete file
        $file = PMCS::get_storage_dir() . '/' . $file_name;
        unlink($file);

In addition, the delete function does not restrict file paths to the snippet folder, allowing directory traversal. This means that attackers can specify any file on the server to be read and then subsequently deleted. This ultimately makes it possible for unauthenticated attackers to delete any arbitrary file on the server, including the site’s wp-config.php file. Deleting wp-config.php forces the site into a setup state, allowing an attacker to initiate a site takeover by connecting it to a database under their control.

The Patch

The vendor patched this issue by adding a file name sanitize helper function with the normalize_snippet_file_name() function. This means that the path is restricted to the snippet subdirectory in the WordPress uploads directory.

Also, the vendor added a capability check and a nonce check to the action_handler() function. This means that only administrators can access the function.

public static function action_handler()
{
    if(!empty($_GET['page']) && $_GET['page'] == 'perfmatters') {

        //permissions and network scope check
        if(!current_user_can('manage_options') || !perfmatters_network_access()) {
            return;
        }

        global $pmcs_error;

        //bulk actions
        if(!empty($_GET['action2']) && !empty($_GET['snippets'])) {
            self::verify_action_nonce('pmcs_nonce', 'pmcs-action');
            $snippet_files = self::normalize_snippet_file_names($_GET['snippets']);

            if(empty($snippet_files)) {
                self::admin_notice_redirect('action2', 'file_not_found');
            }
//normalize one snippet filename and reject invalid values
public static function normalize_snippet_file_name($file_name)
{
    if(!is_scalar($file_name)) {
        return '';
    }

    $file_name = basename(sanitize_file_name((string) $file_name));

    if(empty($file_name) || $file_name === 'index.php' || !preg_match('/.php$/i', $file_name)) {
        return '';
    }

    return $file_name;
}

//normalize a list of snippet filenames
public static function normalize_snippet_file_names($file_names)
{
    $normalized = [];

    foreach((array) $file_names as $file_name) {
        $file_name = self::normalize_snippet_file_name($file_name);
        if(!empty($file_name)) {
            $normalized[] = $file_name;
        }
    }

    return array_values(array_unique($normalized));
}
//delete snippet, supports multiple
public static function delete($file_names)
{
	//get config
    $config = PMCS::get_snippet_config();

    foreach(PMCS::normalize_snippet_file_names($file_names) as $file_name) {

        //get snippet data
        $snippet = self::get($file_name);

        //delete cached files if needed
        if(!empty($snippet['meta']['type']) && in_array($snippet['meta']['type'], ['js', 'css'])) {
            $cached_files = PMCS::get_storage_dir() . '/' . $snippet['meta']['type'] . '/' . str_replace('.php', '*', $file_name);
            foreach(glob($cached_files) as $cached_file) {
			    unlink($cached_file);
			}
        }

        //delete file
        $file = PMCS::get_storage_dir() . '/' . $file_name;
        unlink($file);

Disclosure Timeline

March 1, 2026 – We received the submission for the Arbitrary File Deletion vulnerability in Perfmatters via the Wordfence Bug Bounty Program.
March 17, 2026 – We validated the report and confirmed the proof-of-concept exploit.
March 17, 2026 – We initiated contact via the vendor contact form, asking that they confirm the inbox for handling the discussion.
March 17, 2026 – The vendor registered on our Wordfence Vulnerability Management Portal for WordPress vendors.
March 19, 2026 – The full disclosure details are sent instantly to the vendor upon registering and verifying ownership of their software. The vendor acknowledged the report and began working on a fix.
March 25, 2026 – The fully patched version of the plugin, 2.6.0, was released.

Conclusion

In this blog post, we detailed an Arbitrary File Deletion vulnerability within the Perfmatters plugin affecting versions 2.5.9.1 and earlier. This vulnerability allows unauthenticated threat actors to delete arbitrary files on the server which can be leveraged to achieve remote code execution and lead to complete site compromise. The vulnerability has been addressed in version 2.6.0 of the plugin.

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

All Wordfence users, including those running Wordfence Premium, Wordfence Care, and Wordfence Response, as well as sites running the free version of Wordfence, are fully protected against this vulnerability by the Wordfence firewall’s built-in Local File Inclusion protection.

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 200,000 WordPress Sites Affected by Arbitrary File Deletion Vulnerability in Perfmatters WordPress Plugin appeared first on Wordfence.

Leave a Comment