CVE-2026-4949

ProfilePress <= 4.16.12 - Missing Authorization to Authenticated (Subscriber+) Inactive Membership Plan Subscription

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
4.16.13
Patched in
1d
Time to patch

Description

The Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress plugin for WordPress is vulnerable to Missing Authorization in all versions up to, and including, 4.16.12. This is due to the 'process_checkout' function not properly enforcing the plan active status check when a 'change_plan_sub_id' parameter is provided. This makes it possible for authenticated attackers, with Subscriber-level access and above, to subscribe to inactive membership plans by supplying an arbitrary 'change_plan_sub_id' value in the checkout request.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=4.16.12
PublishedApril 15, 2026
Last updatedApril 15, 2026
Affected pluginwp-user-avatar

What Changed in the Fix

Changes introduced in v4.16.13

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4949 (ProfilePress Inactive Membership Subscription) ## 1. Vulnerability Summary The **ProfilePress (formerly WP User Avatar)** plugin is vulnerable to a Missing Authorization flaw in its checkout processing logic. Specifically, the `process_checkout` function…

Show full research plan

Exploitation Research Plan: CVE-2026-4949 (ProfilePress Inactive Membership Subscription)

1. Vulnerability Summary

The ProfilePress (formerly WP User Avatar) plugin is vulnerable to a Missing Authorization flaw in its checkout processing logic. Specifically, the process_checkout function (found in the Membership module) fails to verify if a membership plan is currently active when a user provides the change_plan_sub_id parameter. This parameter is intended for users upgrading or downgrading an existing subscription. By supplying this parameter, an authenticated attacker (Subscriber level) can bypass the "Active" status check and successfully subscribe to plans that the site administrator has deactivated or hidden.

2. Attack Vector Analysis

  • Endpoint: The checkout page containing the [ppress-checkout] shortcode. Usually /checkout/ or a custom-defined page.
  • Hook/Action: The checkout processing is typically triggered by a POST request to the checkout URL or via the ppress_process_checkout action.
  • Vulnerable Parameter: change_plan_sub_id (used to indicate a plan switch).
  • Other Parameters: plan_id (the ID of the inactive plan), ppress_checkout_nonce (CSRF protection).
  • Authentication: Required (Subscriber or higher). The attacker must have an existing subscription (even a free/low-tier one) to obtain a valid change_plan_sub_id.

3. Code Flow (Inferred)

  1. Entry Point: User submits the checkout form.
  2. Controller: ProfilePress\Core\Membership\Controllers\CheckoutController::process_checkout() is invoked.
  3. Plan Loading: The code retrieves the plan object using PlanFactory::fromId($_POST['plan_id']).
  4. Vulnerable Condition: The logic checks if isset($_POST['change_plan_sub_id']).
  5. Authorization Bypass: If the parameter is present, the code assumes a "Plan Change" context. In versions <= 4.16.12, this branch skips or bypasses the is_active() check on the target plan_id that is normally performed for new subscriptions.
  6. Subscription Creation: The plugin processes the "change" and updates the user's subscription record to the inactive plan_id.

4. Nonce Acquisition Strategy

The checkout process requires a nonce for security. ProfilePress localizes this nonce into a JavaScript object.

  1. Identify Shortcode: The plugin uses [ppress-checkout].
  2. Create Setup Page:
    wp post create --post_type=page --post_title="Checkout" --post_status=publish --post_content='[ppress-checkout]'
    
  3. Acquire Nonce:
    • Navigate to the newly created checkout page.
    • ProfilePress enqueues ppress-checkout scripts and localizes data into ppress_checkout_params.
    • Use browser_eval to extract it:
      browser_eval("ppress_checkout_params.checkout_nonce")
      
    • Note: If ppress_checkout_params is not found, check for pp_ajax_obj or search the HTML source for ppress_checkout_nonce.

5. Exploitation Strategy

Step 1: Preparation (Admin context)

  1. Create an Active Plan (ID 1, Free).
  2. Create an Inactive Plan (ID 2, Free).
  3. Create a Subscriber user.

Step 2: Establish Initial Subscription (Subscriber context)

  1. Log in as the Subscriber.
  2. Subscribe to the Active Plan (ID 1).
  3. Navigate to the "My Account" page (shortcode [ppress-my-account]) to find the Subscription ID.
    • Alternatively, retrieve it via wp_cli: wp db query "SELECT id FROM wp_ppress_subscriptions WHERE user_id = [USER_ID]"

Step 3: Trigger Plan Change to Inactive Plan

  1. Perform a POST request to the checkout page.
  2. Target the Inactive Plan (ID 2) while referencing the active Subscription ID.

HTTP Request Payload:

  • URL: http://localhost:8080/checkout/ (or the specific checkout slug)
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • plan_id: [INACTIVE_PLAN_ID]
    • change_plan_sub_id: [EXISTING_SUBSCRIPTION_ID]
    • ppress_checkout_nonce: [EXTRACTED_NONCE]
    • ppress_process_checkout: true
    • ppress_payment_method: manual (or any available gateway)

6. Test Data Setup

  1. Plan A (Active): Name: "Basic", Price: 0, Status: Active.
  2. Plan B (Inactive): Name: "Secret Inactive", Price: 0, Status: Inactive.
  3. Checkout Page: Page with [ppress-checkout].
  4. Subscriber User: Username victim, Password password123.
  5. Existing Subscription: User victim subscribed to Plan A.

7. Expected Results

  • The server response should indicate success (e.g., a redirect to a "Success" page or a 200 OK with success messaging).
  • Even though Plan B is marked as "Inactive", the checkout logic should proceed without returning an error like "This plan is not available for purchase."

8. Verification Steps

  1. Check Subscription State:
    wp db query "SELECT plan_id, status FROM wp_ppress_subscriptions WHERE user_id = [USER_ID]"
    
    • Success: The plan_id matches the Inactive Plan ID.
  2. Check Plan Metadata:
    wp db query "SELECT status FROM wp_ppress_plans WHERE id = [INACTIVE_PLAN_ID]"
    
    • Confirm the plan was indeed inactive (status = false or inactive).

9. Alternative Approaches

  • URL Parameter Trick: ProfilePress often supports pre-selecting plans via query strings. Try navigating to /checkout/?plan_id=[INACTIVE_ID]&change_plan_sub_id=[SUB_ID] and clicking the "Place Order" button manually using browser_click.
  • Payment Method: If "manual" (Bank Transfer) is not enabled, the exploit might require selecting "stripe" or "paypal" parameters. Ensure a free payment method or the "Bank Transfer" gateway is enabled in ProfilePress settings for the PoC to minimize complexity.
Research Findings
Static analysis — not yet PoC-verified

Summary

The ProfilePress plugin for WordPress is vulnerable to an authorization bypass in its checkout processing logic. Authenticated users can subscribe to inactive or hidden membership plans by supplying a 'change_plan_sub_id' parameter, which causes the plugin to skip the mandatory status check that normally prevents users from purchasing deactivated plans.

Vulnerable Code

// src/Core/Membership/Controllers/CheckoutController.php

public function process_checkout() {
    $plan_id = absint($_POST['plan_id']);
    $plan = PlanFactory::fromId($plan_id);

    if (isset($_POST['change_plan_sub_id'])) {
        // Vulnerable logic: When change_plan_sub_id is provided, the plugin proceeds 
        // to process the plan change without verifying if the target plan is active.
        $this->process_plan_change($plan, $_POST['change_plan_sub_id']);
    } else {
        // Active check is only performed for new subscriptions
        if (!$plan->is_active()) {
            throw new \Exception(esc_html__('This plan is not active.', 'wp-user-avatar'));
        }
    }
}

Security Fix

--- a/src/Core/Membership/Controllers/CheckoutController.php
+++ b/src/Core/Membership/Controllers/CheckoutController.php
@@ -156,6 +156,10 @@
 
         $plan = PlanFactory::fromId($plan_id);
 
+        if (!$plan->is_active()) {
+            throw new \Exception(esc_html__('The selected plan is not active.', 'wp-user-avatar'));
+        }
+
         if (isset($_POST['change_plan_sub_id'])) {
             $this->process_plan_change($plan, $_POST['change_plan_sub_id']);
         } else {

Exploit Outline

The exploit requires a Subscriber-level account with an existing membership subscription (even a free one). 1. Log in as a Subscriber and identify the existing Subscription ID (e.g., via the 'My Account' page). 2. Obtain a valid checkout nonce from the frontend, typically found in the 'ppress_checkout_params' JavaScript object on the checkout page. 3. Identify the ID of an 'Inactive' membership plan that should be inaccessible. 4. Craft a POST request to the checkout endpoint (e.g., `/checkout/`) with the following parameters: - `plan_id`: The ID of the inactive plan. - `change_plan_sub_id`: The existing subscription ID. - `ppress_checkout_nonce`: The extracted nonce. - `ppress_process_checkout`: true - `ppress_payment_method`: manual (or any enabled gateway) 5. Upon submission, the plugin will successfully transition the user's subscription to the inactive plan, bypassing the 'Active' status validation.

Check if your site is affected.

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