The Exploit
An unauthenticated attacker with knowledge of a victim's email address can unsubscribe them from arbitrary mailing lists by brute-forcing a 32-character MD5 hash derived from the subscriber ID and email. No authentication, session, or nonce is required.
## Attacker knows victim email: [email protected]
## Attacker guesses subscriber ID (typically sequential, starting from 1)
## Attacker computes MD5 of "[email protected]" and requests unsubscribe
curl -X GET "http://target.wordpress/wp-admin/admin-ajax.php?action=sgpbGetUnsubscribeForm&[email protected]&popup=1&sgpbUnsubscribe=5d41402abc4b2a76b9719d911017c592" \
-H "User-Agent: Mozilla/5.0"
## If MD5(subscriberId + email) matches the hardcoded token, the unsubscribe succeeds silently
## Attacker observes HTTP 200 with form submission success, or direct database mutation if AJAX endpoint auto-processes
The attacker observes a successful form submission or an unsubscribe confirmation page. The victim is removed from the mailing list without clicking an email link or confirming their intent. Because the token is deterministic and never stored, any attacker with a subscriber list can mass-unsubscribe targets by iterating subscriber IDs and computing MD5 hashes in parallel.
What the Patch Did
Before:
$receivedToken = $params['token'];
$realToken = md5($params['subscriberId'].$params['email']);
if ($receivedToken == $realToken) {
return true;
}
After:
global $wpdb;
$subscribersTableName = $wpdb->prefix.SGPB_SUBSCRIBERS_TABLE_NAME;
// Get the stored token from database
$subscriber = $wpdb->get_row( $wpdb->prepare( "SELECT unsubscribe_token FROM $subscribersTableName WHERE id = %d", $params['subscriberId'] ), ARRAY_A );
if (empty($subscriber)) {
return false;
}
$receivedToken = isset($params['token']) ? $params['token'] : '';
$storedToken = isset($subscriber['unsubscribe_token']) ? $subscriber['unsubscribe_token'] : '';
// SECURITY: Old MD5 tokens are no longer accepted
if (empty($storedToken)) {
return false;
}
// Use secure comparison to prevent timing attacks
return hash_equals($storedToken, $receivedToken);
The patch replaces client-side token generation with a cryptographically secure token stored in the database during subscriber registration. Instead of deriving a token from predictable subscriber metadata using MD5, the plugin now generates each token once, persists it, and validates incoming tokens against the stored value using hash_equals() to prevent timing-based attacks. The token is no longer computable by an attacker.
Root Cause
CWE-330: Use of Insufficiently Random Values. The vulnerability flows from the unsubscribe endpoint (sgpbGetUnsubscribeForm action) accepting a sgpbUnsubscribe parameter that is compared directly against an MD5 hash of concatenated subscriber ID and email. Both values are attacker-knowable: subscriber IDs are sequential integers enumerable via email enumeration, and emails are supplied by the attacker in the request. The token is computed deterministically and never validated against a server-side authoritative source. An attacker observing or guessing one unsubscribe token can compute all others for any email in the system by iterating subscriber IDs from 1 upward and precomputing MD5(id+email) for each candidate.
Why It Works
The load-bearing line is this one:
return hash_equals($storedToken, $receivedToken);
If you removed it and returned to loose == comparison, timing attacks become viable. If you removed database lookup entirely and went back to MD5 computation, brute-forcing resumes. The database lookup ($wpdb->get_row()) is the enforcement point—it requires the plugin to have issued a token at registration time and stored it durably. Without that mutation, an attacker can compute tokens offline. The hash_equals() function is defensive hardening: it ensures that even if an attacker can guess or forge a token, they cannot leak information about token format or length via response timing. The empty-check guards backward compatibility: old MD5 tokens in the database return false, forcing users onto new secure unsubscribe flows. Together, these layers eliminate predictability, eliminate offline computation, and eliminate timing leaks.
Hardening Checklist
- Generate unsubscribe tokens at subscriber creation time using
wp_generate_password( 32, true )and store them in a dedicatedunsubscribe_tokencolumn. Never compute tokens from user input or subscriber metadata. - Always compare security-sensitive tokens using
hash_equals()instead of==or===to eliminate timing side-channels. - Validate all security tokens against a server-side authoritative source (database, transient, or option) before granting access to protected actions. Never accept tokens computed on the client side.
- Use
$wpdb->prepare()with%dand%splaceholders for all subscriber ID and email queries to prevent SQL injection when building dynamic queries, even if the filter hook appears to be internal. - Add ABSPATH guard at the top of every helper class with
defined( 'ABSPATH' ) || exit;to prevent direct HTTP access to plugin files and limit exposure surface.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13079