CVE-2026-25417

ProfileGrid – User Profiles, Groups and Communities <= 5.9.8.1 - Authenticated (Subscriber+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
5.9.8.2
Patched in
4d
Time to patch

Description

The ProfileGrid – User Profiles, Groups and Communities plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 5.9.8.1 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with subscriber-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.9.8.1
PublishedMarch 23, 2026
Last updatedMarch 26, 2026

What Changed in the Fix

Changes introduced in v5.9.8.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-25417 (ProfileGrid Stored XSS) ## 1. Vulnerability Summary The **ProfileGrid** plugin for WordPress is vulnerable to **Stored Cross-Site Scripting (XSS)** in versions up to and including 5.9.8.1. The vulnerability stems from two primary issues: 1. **Broken Se…

Show full research plan

Exploitation Research Plan: CVE-2026-25417 (ProfileGrid Stored XSS)

1. Vulnerability Summary

The ProfileGrid plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to and including 5.9.8.1. The vulnerability stems from two primary issues:

  1. Broken Security Logic: In admin/class-profile-magic-access-options.php, the function profile_magic_save_access_meta contains a logical error in its nonce verification. It uses if ( sanitize_text_field( $post['pg_meta_box_nonce'] ) || wp_verify_nonce(...) ), where the OR condition ensures that any non-empty string provided for pg_meta_box_nonce will satisfy the check, effectively bypassing CSRF and authorization controls for meta updates.
  2. Unescaped Output: In the same file, the function profile_magic_check_content_access retrieves the post meta pm_admin_note_content and prepends/appends it to the post content without any sanitization or escaping (e.g., esc_html or wp_kses).

Authenticated attackers with Subscriber-level permissions can exploit this to inject scripts into "Blogs" (the profilegrid_blogs custom post type) they own, which will execute for any user viewing the post.

2. Attack Vector Analysis

  • Endpoint: WordPress save_post hook (triggered via post creation or update).
  • Vulnerable Post Type: profilegrid_blogs (User Blogs).
  • Vulnerable Parameter: pm_admin_note_content (Post Meta).
  • Bypass Parameter: pg_meta_box_nonce (set to any non-empty string).
  • Authentication: Subscriber or above (users who can create blogs).
  • Preconditions: The "User Blogs" feature must be enabled, or the attacker must be able to create a profilegrid_blogs post.

3. Code Flow

  1. Entry Point: An authenticated user submits a request to create or update a profilegrid_blogs post. This can occur via the standard WordPress admin or the ProfileGrid frontend blog submission form (usually via shortcode [pg_user_blog]).
  2. Hook Trigger: WordPress triggers the save_post action.
  3. Vulnerable Function: Profile_Magic_access_options::profile_magic_save_access_meta( $post_id ) is called.
  4. Bypass: The code checks if ( sanitize_text_field( $post['pg_meta_box_nonce'] ) || ... ). Since sanitize_text_field returns a string, and a non-empty string evaluates to true, the meta-saving logic proceeds.
  5. Storage: The function updates the pm_admin_note_content meta key with the raw value from $_POST['pm_admin_note_content'] (this logic was likely in the truncated portion of the provided source, but is confirmed by the sink).
  6. Sink: When a user views the post, Profile_Magic_access_options::profile_magic_check_content_access( $content ) is called (hooked to the_content).
  7. Execution: The function retrieves the meta: $admin_note = get_post_meta( $id, 'pm_admin_note_content', true );. It then constructs a div: $note = '<div class="pg-admin-note">' . $admin_note . '</div>'; and modifies $content with it. The payload executes in the viewer's browser.

4. Nonce Acquisition Strategy

The vulnerability effectively bypasses the nonce check. In profile_magic_save_access_meta, the check is:

if ( sanitize_text_field( $post['pg_meta_box_nonce'] ) || wp_verify_nonce(...) )

Because of the || (OR) operator, if pg_meta_box_nonce is present in the request and is not an empty string, sanitize_text_field returns a truthy value, and wp_verify_nonce is never even evaluated or its failure is ignored.

Strategy:

  • No valid nonce is required. Simply include pg_meta_box_nonce=anything in the POST request.

5. Exploitation Strategy

Step 1: Create a Blog Post as a Subscriber

As a Subscriber, create a profilegrid_blogs post to obtain a post_id. This can be done via the frontend dashboard provided by ProfileGrid or via WP-CLI for testing.

Step 2: Inject the Malicious Meta

Send an authenticated POST request to the WordPress save_post entry point (or the ProfileGrid AJAX handler if specific) containing the XSS payload.

Request Details:

  • URL: https://<target>/wp-admin/post.php (if the subscriber can access the dashboard) OR the ProfileGrid frontend submission URL.
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • action: editpost
    • post_ID: <TARGET_POST_ID>
    • pg_meta_box_nonce: bypass_check (any string)
    • pm_admin_note_content: <script>alert(document.domain)</script>
    • pm_admin_note_position: top

Step 3: Trigger the XSS

Navigate to the permalink of the profilegrid_blogs post.

6. Test Data Setup

  1. Enable User Blogs: Ensure ProfileGrid is configured to allow users to submit blogs.
  2. Create Subscriber: Create a user with the subscriber role.
  3. Create Blog Post:
    # As an admin, create a blog post for the subscriber
    wp post create --post_type=profilegrid_blogs --post_status=publish --post_title="XSS Test Post" --post_author=<SUBSCRIBER_ID>
    
  4. Identify Post ID: Use wp post list --post_type=profilegrid_blogs to find the ID.

7. Expected Results

  • The server should process the update request and return a 200 or 302 (redirect).
  • When viewing the post, the HTML source should contain:
    <div class="pg-admin-note"><script>alert(document.domain)</script></div>
  • The browser should trigger the alert box.

8. Verification Steps

After the exploit attempt, verify the database state using WP-CLI:

# Check if the malicious meta was stored
wp post meta get <POST_ID> pm_admin_note_content

9. Alternative Approaches

If the Subscriber cannot reach wp-admin/post.php, the ProfileGrid frontend submission logic must be used.

  • Shortcode for Nonce (if the bypass is patched in some parts): If a valid nonce is needed for the frontend form, use browser_navigate to a page with [pg_user_blog] and extract pm_ajax_object.nonce.
  • AJAX Handler: Check for AJAX actions registered in Profile_Magic_Public. The localized object pm_ajax_object in public/class-profile-magic-public.php suggests AJAX-heavy interaction.
  • Parameter variations: Try pm_content_access or pm_content_access_group if pm_admin_note_content is filtered, although the source shows the admin note is the most likely unescaped sink.

Check if your site is affected.

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