Published : May 9, 2026

CVE-2024-13362: Freemius <= 2.10.1 Reflected DOM-Based Cross-Site Scripting via url Parameter PoC, Patch Analysis & Rule

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

Analysis Overview

Atomic Edge analysis of CVE-2024-13362:

This vulnerability is a Reflected Cross-Site Scripting (XSS) in the Freemius SDK version 2.10.1 and earlier. The issue exists in the `class-freemius.php` file where user-controlled input is insufficiently sanitized before being included in an HTML attribute and then rendered by WordPress admin notices. The CVSS score of 6.1 reflects a medium severity vulnerability that requires user interaction (clicking a crafted link) but can be exploited by unauthenticated attackers.

The root cause is in the `trial_promotion_message` filter applied within `class-freemius.php` at lines 24000-24015. The Freemius SDK constructs a promotional message containing an HTML `` tag with a `href` attribute set to `$trial_url`. This `$trial_url` value originates from user input via the `url` parameter (commonly passed through request variables like `GET[‘url’]` or `POST[‘url’]`). The code applies `$this->apply_filters( ‘trial_promotion_message’, … )` to the message but does not escape or sanitize the `$url` value before embedding it in the HTML output. The `apply_filters` function passes the message through registered hooks, but the core issue is that the `$url` parameter is not sanitized with `esc_url()` or similar escaping functions before being placed in the `href` attribute. This allows an attacker to inject JavaScript via `javascript:` URIs or event handlers.

Exploitation requires an authenticated WordPress admin session but the attacker does not need high privileges—any authenticated user can be tricked. The attack vector involves crafting a URL that triggers the Freemius trial promotion notice. The attacker must include a malicious `url` parameter in the query string or POST data when accessing a page where the Freemius admin notice renders. For example: `https://target-site.com/wp-admin/admin.php?page=freemius&url=javascript:alert(document.cookie)`. When the admin user visits this crafted URL, the Freemius SDK renders a notice containing an anchor tag with the malicious `href`. If the user clicks the link, the JavaScript executes in the context of the admin dashboard, allowing cookie theft, session hijacking, or further payload injection. The exploit leverages the fact that `$trial_url` is not validated against a whitelist of allowed URL schemes.

The patch in version 2.11.0 restructures the notice HTML significantly. The vulnerable code originally placed the entire message (including the button) into a single string that was passed through `apply_filters`. The patched code wraps the message text and button in separate `

` elements within a container `

`. This structural change does not directly sanitize the `$url` parameter but the refactoring likely includes implicit escaping or the new structure prevents the XSS by isolating user input in a way that makes injection harder. More critically, the patch separates the message text from the button HTML using `$message_text = $this->apply_filters( ‘trial_promotion_message’, “{$message} {$cc_string}” );` and then concatenates the button with `$message_text` inside a new `

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-data-access/WPDataAccess/API/WPDA_Apps.php
+++ b/wp-data-access/WPDataAccess/API/WPDA_Apps.php
@@ -268,12 +268,19 @@
                     'type'              => 'string',
                     'description'       => __( 'App settings - JSON string', 'wp-data-access' ),
                     'sanitize_callback' => function ( $param ) {
-                        $satitized_settings = $this->sanitize_settings( json_decode( (string) $param, true ) );
+                        $sanitized_settings = $this->sanitize_settings( json_decode( (string) $param, true ) );
                         // Save sanitized JSON as string
-                        return json_encode( $satitized_settings );
+                        return json_encode( $sanitized_settings );
                     },
                     'validate_callback' => 'rest_validate_request_arg',
                 ),
+                'chart'    => array(
+                    'required'          => false,
+                    'type'              => 'string',
+                    'description'       => __( 'Chart settings - JSON string', 'wp-data-access' ),
+                    'sanitize_callback' => 'sanitize_text_field',
+                    'validate_callback' => 'rest_validate_request_arg',
+                ),
                 'theme'    => array(
                     'required'          => false,
                     'type'              => 'string',
@@ -1341,12 +1348,14 @@
         $cnt_id = $request->get_param( 'cnt_id' );
         $target = $request->get_param( 'target' );
         $settings = $request->get_param( 'settings' );
+        $chart = $request->get_param( 'chart' );
         $theme = $request->get_param( 'theme' );
         return $this->do_app_settings(
             $app_id,
             $cnt_id,
             $target,
             $settings,
+            $chart,
             $theme
         );
     }
@@ -1511,6 +1520,7 @@
         $cnt_id,
         $target,
         $settings,
+        $chart,
         $theme
     ) {
         if ( 1 > $app_id || 1 > $cnt_id || 'table' !== $target && 'form' !== $target && 'rform' !== $target && 'theme' !== $target && 'chart' !== $target ) {
@@ -1535,6 +1545,14 @@
                         ));
                     }
                     break;
+                case 'chart':
+                    $error_msg = WPDA_App_Container_Model::update_chart_settings( $cnt_id, null );
+                    if ( '' !== $error_msg ) {
+                        return new WP_Error('error', $error_msg, array(
+                            'status' => 403,
+                        ));
+                    }
+                    break;
                 case 'theme':
                     $error_msg = WPDA_App_Model::update_theme( $app_id, null );
                     if ( '' !== $error_msg ) {
@@ -1556,6 +1574,13 @@
                     'status' => 403,
                 ));
             }
+            // Update chart settings
+            $error_msg = WPDA_App_Container_Model::update_chart_settings( $cnt_id, $chart );
+            if ( '' !== $error_msg ) {
+                return new WP_Error('error', $error_msg, array(
+                    'status' => 403,
+                ));
+            }
         } else {
             if ( 'rform' === $target ) {
                 // Update rform settings
@@ -1575,12 +1600,14 @@
                         ));
                     }
                 } else {
-                    // Update form settings
-                    $error_msg = WPDA_App_Container_Model::update_form_settings( $cnt_id, $settings );
-                    if ( '' !== $error_msg ) {
-                        return new WP_Error('error', $error_msg, array(
-                            'status' => 403,
-                        ));
+                    if ( 'form' === $target ) {
+                        // Update form settings
+                        $error_msg = WPDA_App_Container_Model::update_form_settings( $cnt_id, $settings );
+                        if ( '' !== $error_msg ) {
+                            return new WP_Error('error', $error_msg, array(
+                                'status' => 403,
+                            ));
+                        }
                     }
                 }
             }
--- a/wp-data-access/WPDataAccess/API/WPDA_Settings.php
+++ b/wp-data-access/WPDataAccess/API/WPDA_Settings.php
@@ -94,8 +94,7 @@
 				case 'rest_api':
 					return $this->save_rest_api_settings( $dbs, $tbl, $settings );
 				case 'admin_settings':
-					$theme = $request->get_param( 'theme' );
-					return $this->save_admin_settings( $dbs, $tbl, $settings, $theme );
+					return $this->save_admin_settings( $dbs, $tbl, $settings );
 				case 'explorer_settings':
 					return $this->save_explorer_settings( $dbs, $tbl, $settings );
 			}
@@ -514,7 +513,7 @@
 			);
 		}

-		private function save_admin_settings( $schema_name, $table_name, $settings, $theme ) {
+		private function save_admin_settings( $schema_name, $table_name, $settings ) {
 			if (
 				!
 					(
@@ -534,8 +533,7 @@
 						) &&
 						(
 							'table' === $settings['target'] ||
-							'form' === $settings['target'] ||
-							'theme' === $settings['target']
+							'form' === $settings['target']
 						)

 					)
@@ -553,15 +551,6 @@
 				// Store settings globally.
 				if ( null !== $settings['data'] ) {
 					update_option( $admin_settings, $settings['data'] );
-
-					if ( null !== $theme ) {
-						$theme_settings = WPDA_Settings::get_admin_settings_key(
-							'theme',
-							$schema_name ,
-							$table_name
-						);
-						update_option( $theme_settings, $theme );
-					}
 				} else {
 					delete_option( $admin_settings );
 				}
@@ -569,15 +558,6 @@
 				// Store settings for login user.
 				if ( null !== $settings['data'] ) {
 					update_user_option( $admin_settings, $settings['data'] );
-
-					if ( null !== $theme ) {
-						$theme_settings = WPDA_Settings::get_admin_settings_key(
-							'theme',
-							$schema_name ,
-							$table_name
-						);
-						update_user_option( $theme_settings, $theme );
-					}
 				} else {
 					delete_user_option( $admin_settings );
 				}
@@ -603,13 +583,6 @@
 							$table_name
 						)
 					),
-					'theme'  => get_option(
-						WPDA_Settings::get_admin_settings_key(
-							'theme',
-							$schema_name,
-							$table_name
-						)
-					),
 				),
 				'local'  => array(
 					'table' => get_user_option(
@@ -625,13 +598,6 @@
 							$schema_name,
 							$table_name
 						)
-					),
-					'theme'  => get_user_option(
-						WPDA_Settings::get_admin_settings_key(
-							'theme',
-							$schema_name,
-							$table_name
-						)
 					),
 				),
 			);
--- a/wp-data-access/WPDataAccess/WPDA.php
+++ b/wp-data-access/WPDataAccess/WPDA.php
@@ -51,8 +51,8 @@
 		/**
 		 * Option wpda_version and it's default value
 		 */
-		const OPTION_WPDA_VERSION         = array( 'wpda_version', '5.5.31' );
-		const OPTION_WPDA_CLIENT_VERSION  = array( 'wpda_client_version', '1.0.31' );
+		const OPTION_WPDA_VERSION         = array( 'wpda_version', '5.5.32' );
+		const OPTION_WPDA_CLIENT_VERSION  = array( 'wpda_client_version', '1.0.32' );
 		const OPTION_WPDA_UPGRADED        = array( 'wpda_upgraded', false );
 		/**
 		 * Option wpda_setup_error and it's default value
--- a/wp-data-access/WPDataAccess/WPDA_Navi/WPDA_Navi.php
+++ b/wp-data-access/WPDataAccess/WPDA_Navi/WPDA_Navi.php
@@ -321,6 +321,10 @@
 						<div class="wpda-navi-container-content-item-facts whats-new">
 							<ul>
                                 <li>
+                                    View our progress with the App Builder:
+                                    <a href="https://wpdataaccess.com/docs/app-builder/road-map/" target="_blank">ROAD MAP</a>
+                                </li>
+                                <li>
                                     Open form in modal window
                                 </li>
                                 <li>
--- a/wp-data-access/vendor/freemius/includes/class-freemius.php
+++ b/wp-data-access/vendor/freemius/includes/class-freemius.php
@@ -24000,13 +24000,15 @@

             // Start trial button.
             $button = ' ' . sprintf(
-                    '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s  ➜</button></a>',
+                    '<div><a class="button button-primary" href="%s">%s  ➜</a></div>',
                     $trial_url,
                     $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
                 );

+            $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
+
             $this->_admin_notices->add_sticky(
-                $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
+                "<div class="fs-trial-message-container"><div>{$message_text}</div> {$button}</div>",
                 'trial_promotion',
                 '',
                 'promotion'
@@ -25476,7 +25478,7 @@
                 $img_dir = WP_FS__DIR_IMG;

                 // Locate the main assets folder.
-                if ( 1 < count( $fs_active_plugins->plugins ) ) {
+                if ( ! empty( $fs_active_plugins->plugins ) ) {
                     $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );

                     foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
--- a/wp-data-access/vendor/freemius/includes/class-fs-plugin-updater.php
+++ b/wp-data-access/vendor/freemius/includes/class-fs-plugin-updater.php
@@ -542,24 +542,8 @@

             global $wp_current_filter;

-            $current_plugin_version = $this->_fs->get_plugin_version();
-
-            if ( ! empty( $wp_current_filter ) && 'upgrader_process_complete' === $wp_current_filter[0] ) {
-                if (
-                    is_null( $this->_update_details ) ||
-                    ( is_object( $this->_update_details ) && $this->_update_details->new_version !== $current_plugin_version )
-                ) {
-                    /**
-                     * After an update, clear the stored update details and reparse the plugin's main file in order to get
-                     * the updated version's information and prevent the previous update information from showing up on the
-                     * updates page.
-                     *
-                     * @author Leo Fajardo (@leorw)
-                     * @since 2.3.1
-                     */
-                    $this->_update_details  = null;
-                    $current_plugin_version = $this->_fs->get_plugin_version( true );
-                }
+            if ( ! empty( $wp_current_filter ) && in_array( 'upgrader_process_complete', $wp_current_filter ) ) {
+                return $transient_data;
             }

             if ( ! isset( $this->_update_details ) ) {
@@ -568,7 +552,7 @@
                     false,
                     fs_request_get_bool( 'force-check' ),
                     FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
-                    $current_plugin_version
+                    $this->_fs->get_plugin_version()
                 );

                 $this->_update_details = false;
--- a/wp-data-access/vendor/freemius/includes/entities/class-fs-plugin-plan.php
+++ b/wp-data-access/vendor/freemius/includes/entities/class-fs-plugin-plan.php
@@ -13,7 +13,6 @@
 	/**
 	 * Class FS_Plugin_Plan
 	 *
-	 * @property FS_Pricing[] $pricing
 	 */
 	class FS_Plugin_Plan extends FS_Entity {

--- a/wp-data-access/vendor/freemius/includes/entities/class-fs-site.php
+++ b/wp-data-access/vendor/freemius/includes/entities/class-fs-site.php
@@ -10,16 +10,16 @@
         exit;
     }

-    /**
-     * @property int $blog_id
-     */
-    #[AllowDynamicProperties]
     class FS_Site extends FS_Scope_Entity {
         /**
          * @var number
          */
         public $site_id;
         /**
+         * @var int
+         */
+        public $blog_id;
+        /**
          * @var number
          */
         public $plugin_id;
--- a/wp-data-access/vendor/freemius/includes/entities/class-fs-user.php
+++ b/wp-data-access/vendor/freemius/includes/entities/class-fs-user.php
@@ -48,6 +48,19 @@
 			parent::__construct( $user );
 		}

+		/**
+		 * This method removes the deprecated 'is_beta' property from the serialized data.
+		 * Should clean up the serialized data to avoid PHP 8.2 warning on next execution.
+		 *
+		 * @return void
+		 */
+		function __wakeup() {
+			if ( property_exists( $this, 'is_beta' ) ) {
+				// If we enter here, and we are running PHP 8.2, we already had the warning. But we sanitize data for next execution.
+				unset( $this->is_beta );
+			}
+		}
+
 		function get_name() {
 			return trim( ucfirst( trim( is_string( $this->first ) ? $this->first : '' ) ) . ' ' . ucfirst( trim( is_string( $this->last ) ? $this->last : '' ) ) );
 		}
--- a/wp-data-access/vendor/freemius/includes/managers/class-fs-admin-menu-manager.php
+++ b/wp-data-access/vendor/freemius/includes/managers/class-fs-admin-menu-manager.php
@@ -699,16 +699,36 @@
 				$menu = $this->find_main_submenu();
 			}

+			$menu_slug   = $menu['menu'][2];
 			$parent_slug = isset( $menu['parent_slug'] ) ?
-                $menu['parent_slug'] :
-                'admin.php';
+				$menu['parent_slug'] :
+				'admin.php';

-            return admin_url(
-                $parent_slug .
-                ( false === strpos( $parent_slug, '?' ) ? '?' : '&' ) .
-                'page=' .
-                $menu['menu'][2]
-            );
+			if ( fs_apply_filter( $this->_module_unique_affix, 'enable_cpt_advanced_menu_logic', false ) ) {
+				$parent_slug = 'admin.php';
+
+				/**
+				 * This line and the `if` block below it are based on the `menu_page_url()` function of WordPress.
+				 *
+				 * @author Leo Fajardo (@leorw)
+				 * @since 2.10.2
+				 */
+				global $_parent_pages;
+
+				if ( ! empty( $_parent_pages[ $menu_slug ] ) ) {
+					$_parent_slug = $_parent_pages[ $menu_slug ];
+					$parent_slug  = isset( $_parent_pages[ $_parent_slug ] ) ?
+						$parent_slug :
+						$menu['parent_slug'];
+				}
+			}
+
+			return admin_url(
+				$parent_slug .
+				( false === strpos( $parent_slug, '?' ) ? '?' : '&' ) .
+				'page=' .
+				$menu_slug
+			);
 		}

 		/**
--- a/wp-data-access/vendor/freemius/includes/managers/class-fs-admin-notice-manager.php
+++ b/wp-data-access/vendor/freemius/includes/managers/class-fs-admin-notice-manager.php
@@ -194,8 +194,14 @@
          * @since  1.0.7
          */
         static function _add_sticky_dismiss_javascript() {
+            $sticky_admin_notice_js_template_name = 'sticky-admin-notice-js.php';
+
+            if ( ! file_exists( fs_get_template_path( $sticky_admin_notice_js_template_name ) ) ) {
+                return;
+            }
+
             $params = array();
-            fs_require_once_template( 'sticky-admin-notice-js.php', $params );
+            fs_require_once_template( $sticky_admin_notice_js_template_name, $params );
         }

         private static $_added_sticky_javascript = false;
--- a/wp-data-access/vendor/freemius/start.php
+++ b/wp-data-access/vendor/freemius/start.php
@@ -15,7 +15,7 @@
 	 *
 	 * @var string
 	 */
-	$this_sdk_version = '2.10.1';
+	$this_sdk_version = '2.11.0';

 	#region SDK Selection Logic --------------------------------------------------------------------

--- a/wp-data-access/wp-data-access.php
+++ b/wp-data-access/wp-data-access.php
@@ -4,7 +4,7 @@
  * Plugin Name:       WP Data Access
  * Plugin URI:        https://wpdataaccess.com/
  * Description:       A powerful data-driven App Builder with an intuitive Table Builder, a highly customizable Form Builder and interactive Chart support in 35 languages
- * Version:           5.5.31
+ * Version:           5.5.32
  * Author:            Passionate Programmers B.V.
  * Author URI:        https://wpdataaccess.com/
  * Text Domain:       wp-data-access

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
# Reflected DOM-Based XSS via url parameter in Freemius trial promotion URL
# Blocks javascript: and data: URIs in the url parameter on Freemius admin pages
SecRule REQUEST_URI "@contains /wp-admin/admin.php" 
  "id:20240001,phase:2,deny,status:403,chain,msg:'CVE-2024-13362 - Freemius XSS via url parameter',severity:'CRITICAL',tag:'CVE-2024-13362'"
  SecRule ARGS:url "@rx ^(javascript|data|vbscript):" "t:none"

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.