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

CVE-2024-13362: Freemius <= 2.10.1 – Reflected DOM-Based Cross-Site Scripting via url Parameter (wp-data-access)

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 `

` element. This separation ensures the `$url` is only placed in the button’s `href` attribute and not in the message text that gets filtered, reducing but not fully eliminating the risk. The definitive fix would require proper URL validation and escaping.

The impact of exploitation is significant for an authenticated reflected XSS. An attacker can steal session cookies, access tokens, or perform actions on behalf of the victim (e.g., creating admin users, installing plugins, modifying content). Since the XSS executes in the WordPress admin context, it gives the attacker full access to the admin dashboard capabilities of the victim user. Combined with social engineering (a convincing link), this can lead to complete site compromise. The vulnerability affects all sites using Freemius SDK 2.10.1, which includes thousands of WordPress plugins and themes that integrate Freemius for licensing and updates.

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

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School