ProfilePress <= 4.16.12 - Missing Authorization to Authenticated (Subscriber+) Inactive Membership Plan Subscription
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:NTechnical Details
<=4.16.12What Changed in the Fix
Changes introduced in v4.16.13
Source Code
WordPress.org SVN# 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
POSTrequest to the checkout URL or via theppress_process_checkoutaction. - 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)
- Entry Point: User submits the checkout form.
- Controller:
ProfilePress\Core\Membership\Controllers\CheckoutController::process_checkout()is invoked. - Plan Loading: The code retrieves the plan object using
PlanFactory::fromId($_POST['plan_id']). - Vulnerable Condition: The logic checks if
isset($_POST['change_plan_sub_id']). - 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 targetplan_idthat is normally performed for new subscriptions. - 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.
- Identify Shortcode: The plugin uses
[ppress-checkout]. - Create Setup Page:
wp post create --post_type=page --post_title="Checkout" --post_status=publish --post_content='[ppress-checkout]' - Acquire Nonce:
- Navigate to the newly created checkout page.
- ProfilePress enqueues
ppress-checkoutscripts and localizes data intoppress_checkout_params. - Use
browser_evalto extract it:browser_eval("ppress_checkout_params.checkout_nonce") - Note: If
ppress_checkout_paramsis not found, check forpp_ajax_objor search the HTML source forppress_checkout_nonce.
5. Exploitation Strategy
Step 1: Preparation (Admin context)
- Create an Active Plan (ID 1, Free).
- Create an Inactive Plan (ID 2, Free).
- Create a Subscriber user.
Step 2: Establish Initial Subscription (Subscriber context)
- Log in as the Subscriber.
- Subscribe to the Active Plan (ID 1).
- 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]"
- Alternatively, retrieve it via
Step 3: Trigger Plan Change to Inactive Plan
- Perform a
POSTrequest to the checkout page. - 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:trueppress_payment_method:manual(or any available gateway)
6. Test Data Setup
- Plan A (Active): Name: "Basic", Price: 0, Status: Active.
- Plan B (Inactive): Name: "Secret Inactive", Price: 0, Status: Inactive.
- Checkout Page: Page with
[ppress-checkout]. - Subscriber User: Username
victim, Passwordpassword123. - Existing Subscription: User
victimsubscribed to Plan A.
7. Expected Results
- The server response should indicate success (e.g., a redirect to a "Success" page or a
200 OKwith 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
- Check Subscription State:
wp db query "SELECT plan_id, status FROM wp_ppress_subscriptions WHERE user_id = [USER_ID]"- Success: The
plan_idmatches the Inactive Plan ID.
- Success: The
- Check Plan Metadata:
wp db query "SELECT status FROM wp_ppress_plans WHERE id = [INACTIVE_PLAN_ID]"- Confirm the plan was indeed inactive (
status=falseorinactive).
- Confirm the plan was indeed 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 usingbrowser_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.
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
@@ -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.