ProfileGrid – User Profiles, Groups and Communities <= 5.9.8.1 - Authenticated (Subscriber+) Stored Cross-Site Scripting
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:NTechnical Details
<=5.9.8.1What Changed in the Fix
Changes introduced in v5.9.8.2
Source Code
WordPress.org SVN# 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:
- Broken Security Logic: In
admin/class-profile-magic-access-options.php, the functionprofile_magic_save_access_metacontains a logical error in its nonce verification. It usesif ( sanitize_text_field( $post['pg_meta_box_nonce'] ) || wp_verify_nonce(...) ), where theORcondition ensures that any non-empty string provided forpg_meta_box_noncewill satisfy the check, effectively bypassing CSRF and authorization controls for meta updates. - Unescaped Output: In the same file, the function
profile_magic_check_content_accessretrieves the post metapm_admin_note_contentand prepends/appends it to the post content without any sanitization or escaping (e.g.,esc_htmlorwp_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_posthook (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_blogspost.
3. Code Flow
- Entry Point: An authenticated user submits a request to create or update a
profilegrid_blogspost. This can occur via the standard WordPress admin or the ProfileGrid frontend blog submission form (usually via shortcode[pg_user_blog]). - Hook Trigger: WordPress triggers the
save_postaction. - Vulnerable Function:
Profile_Magic_access_options::profile_magic_save_access_meta( $post_id )is called. - Bypass: The code checks
if ( sanitize_text_field( $post['pg_meta_box_nonce'] ) || ... ). Sincesanitize_text_fieldreturns a string, and a non-empty string evaluates totrue, the meta-saving logic proceeds. - Storage: The function updates the
pm_admin_note_contentmeta 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). - Sink: When a user views the post,
Profile_Magic_access_options::profile_magic_check_content_access( $content )is called (hooked tothe_content). - 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$contentwith 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=anythingin 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:editpostpost_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
- Enable User Blogs: Ensure ProfileGrid is configured to allow users to submit blogs.
- Create Subscriber: Create a user with the
subscriberrole. - 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> - Identify Post ID: Use
wp post list --post_type=profilegrid_blogsto 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
alertbox.
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_navigateto a page with[pg_user_blog]and extractpm_ajax_object.nonce. - AJAX Handler: Check for AJAX actions registered in
Profile_Magic_Public. The localized objectpm_ajax_objectinpublic/class-profile-magic-public.phpsuggests AJAX-heavy interaction. - Parameter variations: Try
pm_content_accessorpm_content_access_groupifpm_admin_note_contentis 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.