CVE-2026-4609

ProfileGrid <= 5.9.8.4 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Group Joining

highMissing Authorization
7.1
CVSS Score
7.1
CVSS Score
high
Severity
5.9.8.5
Patched in
2d
Time to patch

Description

The ProfileGrid – User Profiles, Groups and Communities plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on the pm_invite_user function in all versions up to, and including, 5.9.8.4. This makes it possible for authenticated attackers, with Subscriber-level access and above, to add themselves or any registered user to any ProfileGrid group, including closed and paid groups, bypassing all authorization and payment gates.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Low
Confidentiality
High
Integrity
None
Availability

Technical Details

Affected versions<=5.9.8.4
PublishedMay 12, 2026
Last updatedMay 13, 2026

What Changed in the Fix

Changes introduced in v5.9.8.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4609 (ProfileGrid Group Joining Bypass) ## 1. Vulnerability Summary The **ProfileGrid** plugin for WordPress (up to and including version 5.9.8.4) contains a missing authorization vulnerability in the `pm_invite_user` function. While this function is intended …

Show full research plan

Exploitation Research Plan: CVE-2026-4609 (ProfileGrid Group Joining Bypass)

1. Vulnerability Summary

The ProfileGrid plugin for WordPress (up to and including version 5.9.8.4) contains a missing authorization vulnerability in the pm_invite_user function. While this function is intended to allow group managers or administrators to invite users to groups, it lacks the necessary capability checks. This allows any authenticated user (starting from the Subscriber role) to invoke this function via an AJAX request to add themselves or any other user to any group, effectively bypassing "Closed" group restrictions and "Paid" group payment gates.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: pm_invite_user
  • HTTP Method: POST
  • Authentication: Authenticated (Subscriber or above)
  • Vulnerable Parameter(s):
    • group_id: The ID of the target ProfileGrid group.
    • user_id: The ID of the user to be added (can be the attacker's own ID).
  • Preconditions:
    • The attacker must be logged in as a Subscriber.
    • The target group ID must be known (groups are often listed on the frontend).

3. Code Flow (Inferred)

  1. Entry Point: The AJAX action wp_ajax_pm_invite_user (and potentially wp_ajax_nopriv_pm_invite_user depending on the implementation, though the CVE specifies "Authenticated") is registered in the plugin initialization.
  2. Handler: The request is routed to the pm_invite_user function (typically located in the PM_request class or a dedicated AJAX handler class).
  3. Missing Check: The function likely calls check_ajax_referer() to verify a nonce but fails to verify if the current_user_id() has permission to manage the group specified by group_id or invite users generally (e.g., current_user_can('manage_options')).
  4. Sink: The function uses the PM_DBhandler (visible in includes/class-profile-magic-dbhandler.php) or update_user_meta to associate the user_id with the group_id in the pm_group meta key (as seen in the logic within admin/partials/add-group-tabview.php).

4. Nonce Acquisition Strategy

ProfileGrid typically enqueues nonces for its AJAX actions via wp_localize_script. Since pm_invite_user is a frontend-facing feature for group managers, the nonce is likely available on group pages or the user's profile.

Strategy:

  1. Identify a page where ProfileGrid scripts are loaded (e.g., a page containing the [pg_groups] or [pg_group_members] shortcode).
  2. Use the browser_eval tool to extract the nonce from the localized JavaScript objects.
  3. Common objects in ProfileGrid include pg_ajax_object or pm_fields_object.

Execution:

  1. Create a dummy page with a ProfileGrid shortcode: wp post create --post_type=page --post_status=publish --post_content='[pg_groups]'.
  2. Navigate to that page as a Subscriber.
  3. Execute JavaScript to find the nonce:
    // Attempt to find the nonce in common ProfileGrid objects
    window.pg_ajax_object?.ajax_nonce || window.pm_fields_object?.nonce
    

5. Exploitation Strategy

Step 1: Reconnaissance

  1. Create a "Closed" group as an Administrator and note its ID (e.g., gid=2).
  2. Create a "Paid" group as an Administrator and note its ID (e.g., gid=3).
  3. Identify the current Subscriber's user ID.

Step 2: Exploitation (Joining the Group)

Submit an AJAX request to add the Subscriber to the restricted group.

Request Details:

  • URL: http://target.local/wp-admin/admin-ajax.php
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=pm_invite_user&group_id=[TARGET_GID]&user_id=[SUBSCRIBER_UID]&_wpnonce=[EXTRACTED_NONCE]
    

Step 3: Persistence/Secondary Exploit

Optionally, attempt to add the Administrator or another user to a group they shouldn't be in to verify "Arbitrary User" addition.

6. Test Data Setup

  1. Target Groups:
    # Create a closed group via WP-CLI (simulating admin action)
    # Note: gid is usually the primary key in the 'wp_pg_groups' table
    wp eval "global \$wpdb; \$wpdb->insert(\$wpdb->prefix . 'pg_groups', ['group_name' => 'Secret Society', 'group_options' => serialize(['group_type' => 'Closed'])]);"
    
  2. Victim/Attacker User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
    
  3. Nonce Page:
    wp post create --post_type=page --post_title="Groups" --post_status=publish --post_content='[pg_groups]'
    

7. Expected Results

  • Response: The server should return a success message (often 1 or a JSON success status).
  • Access: The Subscriber user should now have access to the "Closed" group's content and appear in the group's member list.

8. Verification Steps

  1. Database Check: Check the pm_group user meta for the attacker.
    wp user meta get [ATTACKER_UID] pm_group
    
    Expected Outcome: The meta value (usually a serialized array or string containing IDs) should now include the [TARGET_GID].
  2. UI Check: Log in as the attacker and navigate to the group page to confirm content is visible without a "Join Pending" or "Payment Required" notice.

9. Alternative Approaches

If pm_invite_user is not the correct action for self-joining, investigate:

  • pm_join_group: Often used for Open groups but may lack checks for Closed/Paid groups in the vulnerable version.
  • Parameters: Some versions of ProfileGrid use gid instead of group_id. Verify parameter names by checking admin/js/profile-magic-admin.js for AJAX calls.
Research Findings
Static analysis — not yet PoC-verified

Summary

The ProfileGrid plugin for WordPress is vulnerable to unauthorized access due to a missing authorization check in the pm_invite_user AJAX action handler. Authenticated attackers with Subscriber-level access can exploit this to add themselves or any registered user to any ProfileGrid group, including private and paid groups, bypassing authorization and payment requirements.

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/profilegrid-user-profiles-groups-and-communities/5.9.8.4/admin/class-profile-magic-admin.php /home/deploy/wp-safety.org/data/plugin-versions/profilegrid-user-profiles-groups-and-communities/5.9.8.5/admin/class-profile-magic-admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/profilegrid-user-profiles-groups-and-communities/5.9.8.4/admin/class-profile-magic-admin.php	2026-03-12 09:28:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/profilegrid-user-profiles-groups-and-communities/5.9.8.5/admin/class-profile-magic-admin.php	2026-03-26 10:46:24.000000000 +0000
@@ -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();
 
... (truncated at 18000 of 101674 chars — patch_diff)

Exploit Outline

The exploit targets the pm_invite_user AJAX action, which lacks capability checks for the requesting user. 1. Authentication: The attacker logs in with at least Subscriber-level privileges. 2. Target ID: The attacker identifies the group_id of a target group (e.g., a Restricted or Paid group) from the site's group directory or HTML source. 3. Nonce Retrieval: The attacker extracts a valid AJAX nonce from the localized JavaScript objects pg_ajax_object or pm_fields_object, typically found on pages utilizing ProfileGrid shortcodes. 4. Malicious Request: The attacker sends a POST request to /wp-admin/admin-ajax.php using the following parameters: - action: pm_invite_user - group_id: [Target Group ID] - user_id: [Attacker User ID or Victim User ID] - _wpnonce: [Extracted Nonce] 5. Verification: The server adds the user specified in user_id to the restricted group, granting access to protected content or memberships without payment or admin approval.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.