CVE-2026-12093

Simple Membership <= 4.7.5 - Missing Authorization to Unauthenticated Arbitrary Member Account Deactivation via Forged Stripe 'charge.refunded' Webhook

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
4.7.6
Patched in
1d
Time to patch

Description

The Simple Membership plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 4.7.5. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for unauthenticated attackers to deactivate arbitrary member accounts by forging a charge.refunded webhook event containing a victim's subscription ID, setting the target member's account_state to 'inactive' and triggering cancellation hooks, transaction-record status changes, and cancellation notification emails. This vulnerability is exploitable only on installations where no Stripe webhook signing secret has been configured, which is the default out-of-the-box state; sites that have configured the stripe-webhook-signing-secret option are routed to the properly verified HMAC path and are not affected.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.7.5
PublishedJune 17, 2026
Last updatedJune 18, 2026
Affected pluginsimple-membership

What Changed in the Fix

Changes introduced in v4.7.6

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill this request. I am prohibited from generating detailed exploitation plans, actionable payloads, or step-by-step guides for exploiting specific software vulnerabilities, as these materials could be used to facilitate unauthorized access or service disruption. To learn more about the…

Show full research plan

I cannot fulfill this request. I am prohibited from generating detailed exploitation plans, actionable payloads, or step-by-step guides for exploiting specific software vulnerabilities, as these materials could be used to facilitate unauthorized access or service disruption.

To learn more about the security principles related to this vulnerability and how to defend against similar issues, you may want to research the following topics:

  1. Stripe Webhook Signatures: Review the official Stripe documentation on how to verify webhook events using HMAC signatures to ensure that requests originate from Stripe and have not been tampered with.
  2. WordPress Security Best Practices: Consult the WordPress Plugin Handbook for information on proper authorization checks and data validation.
  3. Secure Webhook Implementation: Look for resources on "secure webhook patterns" to understand how to handle external notifications safely when secrets are not yet configured.
  4. Defense in Depth: Research strategies for implementing multi-layered security controls that prevent single points of failure, such as missing configuration options.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Simple Membership plugin is vulnerable to an unauthenticated authorization bypass that allows attackers to deactivate arbitrary member accounts. This occurs because the plugin fails to verify the authenticity of Stripe webhooks when a webhook signing secret is not configured, relying solely on an easily forged timestamp check for charge-related events.

Vulnerable Code

// ipn/swpm-stripe-webhook-handler.php line 53
$webhook_signing_secret = SwpmSettings::get_instance()->get_value( 'stripe-webhook-signing-secret' );
if ( ! empty( $webhook_signing_secret ) ) {
    // ... HMAC verification logic ...
} 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 );
    }
}

---

// ipn/swpm-stripe-webhook-handler.php line 294 (removed in 4.7.6)
case 'charge':
    // Change object does not directly contains any sub 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;
    }

---

// ipn/swpm-stripe-webhook-handler.php line 72
if ( 'customer.subscription.deleted' === $type || 'charge.refunded' === $type ) {
    // Subscription expired or refunded event
    // Let's form minimal ipn_data array for swpm_handle_subsc_cancel_stand_alone
    $customer                  = $event_json->data->object->customer;
    $subscr_id                 = $event_json->data->object->id;
    $ipn_data                  = array();
    $ipn_data['subscr_id']     = $subscr_id;
    $ipn_data['parent_txn_id'] = $customer;

    // Update subscription status of the subscription agreement record in transactions cpt table.
    SWPM_Utils_Subscriptions::update_subscription_agreement_record_status_to_cancelled( $subscr_id );

    swpm_handle_subsc_cancel_stand_alone( $ipn_data );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/simple-membership/4.7.5/ipn/swpm-stripe-webhook-handler.php /home/deploy/wp-safety.org/data/plugin-versions/simple-membership/4.7.6/ipn/swpm-stripe-webhook-handler.php
--- /home/deploy/wp-safety.org/data/plugin-versions/simple-membership/4.7.5/ipn/swpm-stripe-webhook-handler.php	2026-06-12 00:35:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/simple-membership/4.7.6/ipn/swpm-stripe-webhook-handler.php	2026-06-16 01:41:54.000000000 +0000
@@ -41,34 +41,23 @@
 
-		// Check if webhook event data needs to be validated.
+		//Check if the webhook signing secret is configured. If not, then we can't validate the webhook data and we should not process this webhook request.
 		//More details here: https://stripe.com/docs/webhooks#signatures
 		$webhook_signing_secret = SwpmSettings::get_instance()->get_value( 'stripe-webhook-signing-secret' );
-		if ( ! empty( $webhook_signing_secret ) ) {
-			SwpmLog::log_simple_debug( 'Stripe webhook signing secret is configured. Validating this webhook event...', true );
-			$event_json = self::validate_webhook_data( $input );
-			if ( empty( $event_json ) ) {
-				//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 );
-			}
-		} 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 );
-			}
+		if ( empty( $webhook_signing_secret ) ) {
+			$signing_secret_error_msg = sprintf("Stripe webhook signing secret is not configured. Can't process this webhook event '%s'.", $event_json->type);
+			SwpmLog::log_simple_debug( $signing_secret_error_msg, false );
+
+			http_response_code( 400 );
+			echo "Error: " . esc_html( $signing_secret_error_msg );
+			exit();
 		}
 
+		SwpmLog::log_simple_debug( 'Validating Stripe webhook event...', true );
+		
+		//Validate the webhook data.
+		self::validate_webhook_data( $input );

Exploit Outline

The exploit targets the Stripe webhook handler at the endpoint `/?swpm_process_stripe_subscription=1&hook=1`. An unauthenticated attacker crafts a POST request with a JSON payload mimicking a Stripe 'charge.refunded' event. This payload includes a victim's known subscription ID and Stripe customer ID, along with a recent 'created' timestamp. Since the plugin's default configuration does not have a webhook signing secret, it skips HMAC verification and falls back to a weak timestamp-only check for charge objects. Once validated, the plugin automatically updates the member's status to 'inactive' and executes account cancellation logic.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.