This blog post is about an unauthenticated account takeover vulnerability in the PayU CommercePro plugin. If you're a PayU CommercePro user, please deactivate and delete the plugin.
All Patchstack users are protected from this vulnerability. For plugin developers, we have security audit services and Enterprise API for hosting companies.
About PayU CommercePro plugin
The plugin PayU CommercePro, which has over 5,000 active installations, allows WooCommerce store owners to add PayU payment integration to their shops.

The security vulnerability
In the latest version 3.8.5, the plugin is vulnerable to an account takeover vulnerability, which allows attackers to takeover any user of the WordPress site without authentication. The vulnerability has not been patched yet and is tracked with CVE-2025-31022.
The root of the issue lies in the update_cart_data function:
public function update_cart_data($user_id, $order)
{
global $table_prefix, $wpdb;
$user_session_table = $table_prefix . "woocommerce_sessions";
$shipping_data = array();
if ($order) {
include_once WP_PLUGIN_DIR . '/woocommerce/includes/wc-cart-functions.php';
include_once WP_PLUGIN_DIR . '/woocommerce/includes/wc-notice-functions.php';
include_once WP_PLUGIN_DIR . '/woocommerce/includes/wc-template-hooks.php';
WC()->session = new WC_Session_Handler();
WC()->session->init();
$session = WC()->session->get_session($user_id);
//TRIMMED
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id);
} elseif (!empty($user_id)) {
// Set session for already created/registered user
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id);
}
//TRIMMED
}
return $shipping_data;
}
The function is taking the parameter $user_id
and $order
. After some processing, it checks if the user is logged-in, if not, it is setting the $user_id value as the user.
Tracing where the function update_cart_data() is called, we come across the function handleValidToken()
.
private function handleValidToken($parameters, $email, $txnid)
{
$parameters['address']['state'] = get_state_code_by_name($parameters['address']['state']);
if (!$parameters['address']['state']) {
return [
'status' => 'false',
'data' => [],
'message' => 'The State value is wrong'
];
}
$session_key = $parameters['udf4'];
$order_string = explode('_', $txnid);
$order_id = (int)$order_string[0];
$order = wc_get_order($order_id);
$shipping_address = $parameters['address'];
if (!$email) {
$guest_email = $session_key . '@mailinator.com';
$user_id = $this->payu_create_guest_user($guest_email);
if ($user_id) {
$this->payu_add_new_guest_user_cart_data($user_id, $session_key);
$shipping_data = $this->update_cart_data($user_id, $order);
require_once(ABSPATH . 'wp-admin/includes/user.php');
wp_delete_user($user_id);
}
} else {
if (email_exists($email)) {
$user = get_user_by('email', $email);
$user_id = $user->ID;
$this->payu_add_new_guest_user_cart_data($user_id, $session_key);
$this->update_order_shipping_address($order, $shipping_address, $email);
$shipping_data = $this->update_cart_data($user_id, $order);
}
//TRIMMED
}
//TRIMMED
}
The function update_cart_data
is called on multiple occasions. However, the most interesting part is if (email_exists($email))
statement. It is checking if the provided email exists, and if it does, it's taking the user-input and passing it down to the vulnerable function.
Tracing the function handleValidToken()
towards the source, we see it's called by payuShippingCostCallback
which is a callback for the /payu/v1/get-shipping-cost
API endpoint.
register_rest_route('payu/v1', '/get-shipping-cost', array(
'methods' => ['POST'],
'callback' => array($this, 'payuShippingCostCallback'),
'permission_callback' => '__return_true'
));
public function payuShippingCostCallback(WP_REST_Request $request)
{
$parameters = json_decode($request->get_body(), true);
error_log('shipping api call request ' . $request->get_body());
$email = sanitize_email($parameters['email']);
$txnid = sanitize_text_field($parameters['txnid']);
$auth = apache_request_headers();
$token = $auth['Auth-Token'];
try {
if ($token && $this->payu_validate_authentication_token(PAYU_USER_TOKEN_EMAIL, $token)) {
$response = $this->handleValidToken($parameters, $email, $txnid);
} else {
$response = [
'status' => 'false',
'data' => [],
'message' => 'Token is invalid'
];
return new WP_REST_Response($response, 401);
}
} catch (Throwable $e) {
$response = [
'status' => 'false',
'data' => [],
'message' => 'Fetch Shipping Method Failed (' . $e->getMessage() . ')'
];
return new WP_REST_Response($response, 500);
}
$response_code = $response['status'] == 'false' ? 400 : 200;
error_log('shipping api call response ' . json_encode($response));
return new WP_REST_Response($response, $response_code);
}
We finally have the source from where we can call the vulnerable function update_cart_data()
. It is doing an authentication token check with the constant email PAYU_USER_TOKEN_EMAIL
which is set in the plugin constants: define('PAYU_USER_TOKEN_EMAIL','commerce.pro@payu.in');
private function payu_validate_authentication_token($email, $token)
{
$user_id = get_user_by('email', $email)->ID;
// Get the stored token and expiration time from user meta
$stored_token = get_user_meta($user_id, 'payu_auth_token', true);
$expiration = get_user_meta($user_id, 'payu_auth_token_expiration', true);
// Check if the stored token matches the provided token and is not expired
return ($stored_token === $token && $expiration >= time()) ? true : false;
}
In order to exploit the issue, a valid auth token for the email commerce.pro@payu.in
is needed. Exploring around the other REST API endpoints, we come across /payu/v1/generate-user-token
register_rest_route('payu/v1', '/generate-user-token', array(
'methods' => ['POST'],
'callback' => array($this, 'payu_generate_user_token_callback'),
'permission_callback' => '__return_true'
));
public function payu_generate_user_token_callback(WP_REST_Request $request)
{
// Get and sanitize the email from request
$email = sanitize_email($request->get_param('email'));
//TRIMMED
// Fetch plugin settings
$plugin_data = get_option('woocommerce_payubiz_settings');
$this->payu_salt = isset($plugin_data['currency1_payu_salt']) ? sanitize_text_field($plugin_data['currency1_payu_salt']) : null;
//TRIMMED
// Check if the user exists
if (email_exists($email)) {
$user = get_user_by('email', $email);
$user_id = $user->ID;
// Generate authentication token
$token = $this->payu_generate_authentication_token($user_id);
return new WP_REST_Response([
'status' => true,
'data' => ['token' => $token],
'message' => 'Token Generated',
]);
}
//TRIMMED
}
The endpoint is basically taking the email address as the user-input and returning the auth token for that email. It means we can easily grab the auth token for the hardcoded email address and send the /payu/v1/get-shipping-cost
API request to hit the vulnerable sink update_cart_data()
and takeover any account.
The patch
Since there has been no patch released by the vendor, we decided to go forward with the disclosure after 30 days of responsible disclosure. If/when the vendor pushes the patch, we will update this section accordingly.
Conclusion
It is necessary to ensure that the unauthenticated REST API endpoints are not overly permissive and provide more access to the users. Also, hardcoding sensitive or dynamic information such as email addresses to use it for other cases inside the codebase is not recommended.
Want to learn more about finding and fixing vulnerabilities?
Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.
Timeline
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you're a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you're a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.