The Exploit
Attacker needs a victim who is logged in to the target WordPress site.
curl 'https://TARGET.EXAMPLE/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'action=uc&u=123&e=victim%40example.com&nk=VICTIM_NK'
This request submits the newsletter unsubscribe action with the victim's newsletter key nk but no _wpnonce.
When the request lands, the plugin processes the unsubscribe action and redirects or returns the normal unsubscribe page, even though the request was forged from another site. The subscriber identified by u/e is removed from the newsletter list without requiring a valid nonce token.
What the Patch Did
Before:
$b = '<form action="' . esc_attr($this->build_action_url('uc')) . '" method="post" class="tnp-button-form tnp-unsubscribe">';
$b .= '<input type="hidden" name="nk" value="' . esc_attr($this->get_user_key($user)) . '">';
...
case 'uc':
$this->unsubscribe($user, $email);
After:
$b = '<form action="' . esc_attr($this->build_action_url('uc')) . '" method="post" class="tnp-button-form tnp-unsubscribe">';
$b .= wp_nonce_field('newsletter-unsubscribe', '_wpnonce', true, false);
$b .= '<input type="hidden" name="nk" value="' . esc_attr($this->get_user_key($user)) . '">';
...
case 'uc':
$verified = wp_verify_nonce($_REQUEST['_wpnonce'], 'newsletter-unsubscribe');
if (!$verified) {
$this->redirect($this->build_action_url('u', $user, $email));
}
$this->unsubscribe($user, $email);
The patch adds WordPress nonce support:
wp_nonce_field()to emit_wpnoncein the unsubscribe formwp_verify_nonce()to validate_wpnoncebefore performing the unsubscribe
The same class of fix was applied to the reactivate action path with a separate newsletter-reactivate nonce.
Root Cause
This is CWE-352: Cross-Site Request Forgery. The plugin accepted state-changing POST requests for newsletter unsubscription without validating the WordPress nonce token. Attacker-controlled HTTP parameters (action=uc, nk, and the subscriber identifiers used by build_action_url) reached hook_newsletter_action() and immediately executed unsubscribe().
The unchecked trust boundary is user intent: the plugin trusted that a POST to its unsubscribe action was legitimate because it came from the browser, but it never verified the anti-CSRF token _wpnonce.
Why It Works
The single load-bearing line is the server-side nonce check:
$verified = wp_verify_nonce($_REQUEST['_wpnonce'], 'newsletter-unsubscribe');
If that line is removed, a forged POST is accepted again. The added wp_nonce_field() line alone would not be enough if the handler still skipped verification, because an attacker can still submit any request without the token. The form field is necessary for normal UI submissions; the wp_verify_nonce() call is the actual defence. The redirect on failure is a safe fallback so invalid CSRF attempts do not accidentally execute the unsubscribe path.
Hardening Checklist
- Use
wp_nonce_field()in every HTML form that performs state changes. - Validate the token server-side with
wp_verify_nonce()orcheck_admin_referer()before executing destructive actions. - Never rely on hidden request fields like
nkas CSRF protection by themselves. - If actions are available only to logged-in users, also enforce
is_user_logged_in()and a capability check such ascurrent_user_can('read')or a plugin-specific capability. - For AJAX endpoints, use
check_ajax_referer()instead of manual nonce handling.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-1051