CVE-2026-3309

Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress <= 4.16.11 - Unauthenticated Arbitrary Shortcode Execution via Checkout Billing Fields

mediumImproper Control of Generation of Code ('Code Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
4.16.12
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 arbitrary shortcode execution in all versions up to, and including, 4.16.11. This is due to the plugin allowing user-supplied billing field values from the checkout process to be interpolated into shortcode template strings that are subsequently processed without proper sanitization of shortcode syntax. This makes it possible for unauthenticated attackers to execute arbitrary shortcodes by submitting crafted billing field values during the checkout process.

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.16.11
PublishedApril 3, 2026
Last updatedApril 4, 2026
Affected pluginwp-user-avatar

What Changed in the Fix

Changes introduced in v4.16.12

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to demonstrate unauthenticated arbitrary shortcode execution in ProfilePress <= 4.16.11. ### 1. Vulnerability Summary The ProfilePress plugin is vulnerable to arbitrary shortcode execution via user-supplied billing fields during the checkout process. T…

Show full research plan

This research plan outlines the steps required to demonstrate unauthenticated arbitrary shortcode execution in ProfilePress <= 4.16.11.

1. Vulnerability Summary

The ProfilePress plugin is vulnerable to arbitrary shortcode execution via user-supplied billing fields during the checkout process. The plugin accepts billing information (e.g., first name, last name, address) via AJAX requests and, in some contexts, interpolates these values into strings that are subsequently processed by the WordPress shortcode engine (do_shortcode()). Because the input is not sanitized for shortcode syntax (square brackets), an attacker can inject malicious shortcode tags into billing fields which will be executed by the server.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Actions: ppress_process_checkout or ppress_update_order_review (both have nopriv handlers).
  • Authentication: Unauthenticated.
  • Vulnerable Parameter: Billing fields, specifically ppress_billing_first_name, ppress_billing_last_name, or other ppress_billing_* parameters.
  • Preconditions: A Membership Plan must exist, and a Checkout Page must be configured with the [profilepress-checkout] shortcode to obtain a valid nonce.

3. Code Flow

  1. Entry Point: An unauthenticated user sends an AJAX request to ppress_update_order_review or ppress_process_checkout.
  2. Controller: ProfilePress\Core\Membership\Controllers\CheckoutController (and its CheckoutTrait) handles these actions.
  3. Processing: The plugin captures billing fields from the $_POST superglobal.
  4. Interpolation: During order review updates or processing, these fields are stored in the session/cart and may be used to populate template strings for the "Order Summary" or "Checkout Sidebar".
  5. Sink: The resulting string is passed to do_shortcode(). If a billing field contains [shortcode_name], WordPress will parse and execute it.

4. Nonce Acquisition Strategy

The checkout actions are protected by a nonce named ppress_checkout_nonce.

  1. Setup: Create a Membership Plan and a Checkout Page.
    • wp ppress_plan create --name="Gold" --price=10 (Note: Use WP-CLI to create a plan if the plugin provides a command, otherwise use wp post create for a plan CPT if identified, but standard wp post create for the page is mandatory).
    • wp post create --post_type=page --post_title="Checkout" --post_status=publish --post_content='[profilepress-checkout]'
  2. Navigation: Navigate to the Checkout page using browser_navigate.
  3. Extraction: ProfilePress localizes its parameters in a global JavaScript object. Use browser_eval to extract the nonce.
    • Target Variable: ppress_checkout_params
    • Key: nonce
    • Command: browser_eval("ppress_checkout_params.nonce")

5. Exploitation Strategy

The goal is to trigger shortcode execution via the ppress_update_order_review action, as this typically returns the rendered HTML of the checkout summary, providing immediate feedback.

Step 1: Setup Membership Plan
If a plan doesn't exist, create one to ensure the checkout page renders correctly.

# Example if using standard WP-CLI to create the necessary CPT for a plan
wp post create --post_type=ppress_forms --post_title="Checkout Form" --post_status=publish
# (In a real environment, you'd ensure a plan ID is available)

Step 2: Obtain Nonce and Plan ID
Navigate to the page containing [profilepress-checkout].
Extract plan_id (from URL or page source) and the nonce.

Step 3: Submit Malicious AJAX Request
Send a POST request to admin-ajax.php using the http_request tool.

  • Action: ppress_update_order_review
  • Payload:
    POST /wp-admin/admin-ajax.php HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    action=ppress_update_order_review&
    ppress_checkout_nonce=[NONCE]&
    ppress_billing_first_name=[profilepress-login]&
    ppress_billing_last_name=Tester&
    plan_id=[PLAN_ID]
    
    Note: [profilepress-login] is a standard ProfilePress shortcode that renders a login form. If successful, the response HTML will contain a login form where the first name should be.

6. Test Data Setup

  1. Membership Plan: Create a basic membership plan (ID likely 1).
  2. Checkout Page: A page with [profilepress-checkout] at /checkout/.
  3. Shortcode for PoC: [profilepress-login] or [audio src="https://example.com/test.mp3"] (to see the audio player HTML).

7. Expected Results

  • The HTTP response from the AJAX request should contain the rendered HTML of the injected shortcode.
  • For [profilepress-login], the response should contain form elements like <input name="log" ...>.
  • For [audio], the response should contain <div class="wp-audio-shortcode">.

8. Verification Steps

  1. Inspect Response: Check the JSON response from ppress_update_order_review. The data field usually contains the rendered HTML fragments.
  2. Search for Strings: Look for HTML tags unique to the injected shortcode (e.g., pp-login-form-container).

9. Alternative Approaches

If ppress_update_order_review does not return the rendered field, attempt the exploit via ppress_process_checkout:

  1. Submit the checkout with ppress_billing_first_name set to [profilepress-login].
  2. If the checkout completes (even if payment fails/pending), check the "My Account" page or the Order Confirmation page.
  3. Use wp post list --post_type=ppress_orders to find the created order and check if the shortcode executed in the order notes or summary viewable in the admin dashboard (if testing for Cross-Site Scripting via Shortcode or information disclosure).
Research Findings
Static analysis — not yet PoC-verified

Summary

The ProfilePress plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode execution via checkout billing fields. This occurs because user-supplied billing information, submitted during the checkout AJAX process, is interpolated into template strings (such as the order summary) which are subsequently processed by the WordPress shortcode engine without sanitizing for shortcode syntax.

Vulnerable Code

// src/Membership/Controllers/CheckoutController.php lines 41-42
add_action('wp_ajax_ppress_update_order_review', [$this, 'update_order_review']);
add_action('wp_ajax_nopriv_ppress_update_order_review', [$this, 'update_order_review']);

---

// Within the update_order_review method (typically found in CheckoutTrait included by CheckoutController)
// The plugin processes billing fields from $_POST and renders fragments for the checkout sidebar.
// The vulnerability occurs when billing fields are merged into HTML strings passed to do_shortcode().

$billing_first_name = ppressPOST_var('ppress_billing_first_name', '');
// ... values are stored in session or cart objects ...

// Sink usually involves rendering a template containing the field and parsing it:
$order_summary_html = $this->render_order_summary_template();
echo do_shortcode($order_summary_html);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-user-avatar/4.16.11/src/Membership/Controllers/CheckoutController.php /home/deploy/wp-safety.org/data/plugin-versions/wp-user-avatar/4.16.12/src/Membership/Controllers/CheckoutController.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-user-avatar/4.16.11/src/Membership/Controllers/CheckoutController.php	2026-03-03 12:42:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-user-avatar/4.16.12/src/Membership/Controllers/CheckoutController.php	2026-03-04 12:06:58.000000000 +0000
@@ -300,6 +300,17 @@
                 throw new \Exception(json_encode($customer_id->get_error_messages()));
             }
 
+            $changePlanSub = SubscriptionFactory::fromId($change_plan_sub_id);
+
+            if (
+                $changePlanSub->exists() &&
+                ! empty($customer_id) &&
+                $customer_id !== $changePlanSub->get_customer_id()) {
+                throw new \Exception(
+                    esc_html__('You are not allowed to switch from this plan.', 'wp-user-avatar')
+                );
+            }
+
             $order_id = $this->create_order($customer_id, $cart_vars);
 
             if (is_wp_error($order_id)) {
@@ -331,19 +342,17 @@
 
             } else {
 
-                $sub = SubscriptionFactory::fromId($change_plan_sub_id);
-
-                if ($sub->exists() && $sub->get_customer_id() == $customer_id) {
+                if ($changePlanSub->exists() && $changePlanSub->get_customer_id() == $customer_id) {
 
                     // do not send subscription cancelled email
                     remove_action('ppress_subscription_cancelled', [SubscriptionCancelledNotification::init(), 'dispatch_email'], 10);
                     remove_action('ppress_subscription_expired', [SubscriptionExpiredNotification::init(), 'dispatch_email'], 10);
 
-                    $sub->cancel(true);
-                    $sub->expire();
+                    $changePlanSub->cancel(true);
+                    $changePlanSub->expire();
 
-                    SubscriptionFactory::fromId($subscription_id)->update_meta('_upgraded_from_sub_id', $sub->get_id());
-                    $sub->update_meta('_upgraded_to_sub_id', $subscription_id);
+                    SubscriptionFactory::fromId($subscription_id)->update_meta('_upgraded_from_sub_id', $changePlanSub->get_id());
+                    $changePlanSub->update_meta('_upgraded_to_sub_id', $subscription_id);
                 }
 
                 /** @var CheckoutResponse $process_payment */

Exploit Outline

1. Identify a membership plan ID and navigate to the checkout page containing the [profilepress-checkout] shortcode. 2. Extract the 'ppress_checkout_nonce' from the 'ppress_checkout_params' global JavaScript object on the page. 3. Send an unauthenticated POST request to '/wp-admin/admin-ajax.php' with the action set to 'ppress_update_order_review'. 4. Include the extracted nonce, the plan ID, and a malicious shortcode payload (e.g., '[profilepress-login]') in a billing field parameter like 'ppress_billing_first_name'. 5. The server response will contain rendered HTML fragments for the checkout sidebar, where the injected shortcode has been executed and replaced with its corresponding functional HTML output.

Check if your site is affected.

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