Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 11, 2026

CVE-2026-9281: Master Addons For Elementor <= 3.1.0 Authenticated (Author+) Stored Cross-Site Scripting via 'jtlma_custom_js' Page Setting (Custom JS Extension) PoC, Patch Analysis & Rule

CVE ID CVE-2026-9281
Plugin master-addons
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 3.1.0
Patched Version 3.1.1
Disclosed June 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9281:

This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the Master Addons For Elementor plugin for WordPress, affecting all versions up to and including 3.1.0. The issue resides in the Custom JS Extension, which stores attacker-supplied JavaScript via the ‘jtlma_custom_js’ page setting. An authenticated attacker with Author-level access or above can inject arbitrary web scripts that execute whenever a user visits a compromised page. The CVSS score is 6.4 (Medium), and the CWE is 79 (Improper Neutralization of Input During Web Page Generation).

The root cause is a missing or insufficient capability check during the save process for the ‘jtlma_custom_js’ setting. The plugin only enforces the unfiltered_html capability at the Elementor control registration (UI rendering) stage, not when the value is actually saved via AJAX. The diff shows the plugin moves the Custom JS class from a free module to a pro-only module (config.php lines 2013-2023) and marks it as pro, but the actual vulnerability is that the AJAX handler that processes the elementor_ajax action does not re-verify that the user has the necessary permissions to save arbitrary JavaScript. Author-level users do not have unfiltered_html by default, but the save endpoint trusts the submitted data without filtering.

An attacker with Author credentials can craft a POST request to /wp-admin/admin-ajax.php with the action set to ‘elementor_ajax’. The request includes the page settings with the ‘jtlma_custom_js’ parameter containing malicious JavaScript, such as an XSS payload that steals cookies or redirects users. The Elementor AJAX handler processes the settings and saves them to the post meta without additional sanitization or capability checks beyond the initial UI restriction. This bypasses the intended pro-only restriction and the missing unfiltered_html check.

The patch does not directly modify the vulnerability; instead, it moves the Custom JS feature to the pro version (config.php changes), making it unavailable to free users. Additionally, the patch includes extensive hardening across multiple files: it introduces a new inline script enqueue method to avoid raw echo of user data (popup-frontend.php), adds capability checks to the template and popup AJAX handlers (class-popup-cpt.php, class-importer.php), strips PHP tags from widget builder code (class-rest-controller.php), and uses wp_kses_post on theme builder content (cpt-hooks.php). These changes collectively reduce the attack surface, but the core issue of insufficient save-time checks for the Custom JS setting is addressed primarily by gating the feature behind the pro license.

Successful exploitation allows an attacker to inject persistent JavaScript into any page they can edit. When a victim (including administrators) views the compromised page, the injected script executes in their browser. This can lead to session hijacking, cookie theft, defacement, redirection to malicious sites, or further actions such as creating new admin users or installing malware. The impact is amplified because the injected scripts execute in the context of the WordPress admin dashboard or frontend, depending on where the page is rendered.

Differential between vulnerable and patched code

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

Code Diff
--- a/master-addons/class-master-elementor-addons.php
+++ b/master-addons/class-master-elementor-addons.php
@@ -470,7 +470,6 @@
 					'Shortcode_Manager' => 'class-shortcode-manager.php',
 					'Control_Manager' => 'class-control-manager.php',
 					'Icon_Library_Helper' => 'icon-library-helper.php',
-					'Widget_Builder' => 'widget-builder.php',
 					'Control_Base' => 'controls/class-control-base.php',
 				];
 				if (isset($wb_file_map[$wb_class])) {
@@ -1147,7 +1146,7 @@

 			MasterAddonsIncAdminTheme_BuilderLoader::get_instance();
 			MasterAddonsIncAdminPopupBuilderPopup_Builder_Init::get_instance();
-			// MasterAddonsIncAdminWidgetBuilderWidget_Builder_Init::get_instance();
+			MasterAddonsIncAdminWidgetBuilderWidget_Builder_Init::get_instance();
 			REST_API::get_instance();

 			MasterAddonsIncAdminPage_Importer::get_instance();
--- a/master-addons/inc/admin/config.php
+++ b/master-addons/inc/admin/config.php
@@ -2010,7 +2010,7 @@
                         'custom-js' => [
                             'title'    => 'Custom JS',
                             'icon'     => 'eicon-code-bold',
-                            'class'    => 'MasterAddonsModulesUtilitiesCustomJs',
+                            'class'    => 'MasterAddonsProModulesUtilitiesCustomJs',
                             'demo_url' => 'https://master-addons.com/extension/custom-js/',
                             'docs_url' => 'https://master-addons.com/docs/extensions/custom-js/',
                             'tuts_url' => 'https://www.youtube.com/watch?v=8G4JLw0s8sI',
@@ -2020,7 +2020,7 @@
                                 "Runs only on the target element",
                                 "No extra plugin or coding setup"
                             ],
-                            'is_pro'   => false,
+                            'is_pro'   => true,
                         ],
                         'duplicator' => [
                             'title'    => 'Post/Page Duplicator',
--- a/master-addons/inc/admin/popup-builder/class-popup-cpt.php
+++ b/master-addons/inc/admin/popup-builder/class-popup-cpt.php
@@ -447,6 +447,10 @@
     public function get_shortcode_ajax() {
         check_ajax_referer('jltma_popup_nonce', '_nonce');

+        if ( ! current_user_can( 'edit_posts' ) ) {
+            wp_send_json_error( [ 'message' => __( 'Insufficient permissions', 'master-addons' ) ] );
+        }
+
         $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
         $shortcode = '[jltma_popup id="' . $post_id . '"]';

@@ -541,6 +545,17 @@
             return $allcaps;
         }

+        // Security: only grant the cap if the user already has a base editing
+        // capability through their role. Without this guard, any logged-in user
+        // (incl. subscribers) visiting the Elementor edit URL for a popup would be
+        // granted edit_post for that request — a privilege escalation. With
+        // capability_type='post' on the CPT, authors+ already have edit_posts
+        // natively, so the workaround stays a no-op for them and is fully off for
+        // lower-privileged users.
+        if (empty($user->allcaps['edit_posts']) && empty($user->allcaps['edit_pages'])) {
+            return $allcaps;
+        }
+
         // Grant edit capability for this specific post type
         if (isset($caps[0]) && in_array($caps[0], ['edit_post', 'edit_posts'])) {
             $allcaps[$caps[0]] = true;
--- a/master-addons/inc/admin/popup-builder/class-popup-frontend.php
+++ b/master-addons/inc/admin/popup-builder/class-popup-frontend.php
@@ -504,27 +504,24 @@
         if ($expiration > 0) {
              $max_age = $expiration - time();

-            ?>
-            <script>
-                const cookieName = '<?php echo esc_js($cookie_name); ?>';
-                const value = '<?php echo absint( time() ); ?>';
-                const maxAge = <?php echo (int) $max_age; ?>;
-                const path = '/';
-
-                document.cookie = `${cookieName}=${value}; max-age=${maxAge}; path=${path};`;
-            </script>
-        <?php
-            // setcookie($cookie_name, time(), $expiration, '/');
+            $cookie_js = sprintf(
+                'document.cookie = "%1$s=%2$d; max-age=%3$d; path=/;";',
+                esc_js($cookie_name),
+                absint(time()),
+                (int) $max_age
+            );
+            $this->add_popup_inline_script($cookie_js);
         }

         if ($frequency === 'once_session') {
             // Session cookie (no max-age/expires) — cleared automatically when the browser closes.
             // Avoids PHP sessions, which break full-page caching on the frontend.
-            ?>
-            <script>
-                document.cookie = '<?php echo esc_js($cookie_name); ?>=<?php echo absint(time()); ?>; path=/;';
-            </script>
-            <?php
+            $session_js = sprintf(
+                'document.cookie = "%1$s=%2$d; path=/;";',
+                esc_js($cookie_name),
+                absint(time())
+            );
+            $this->add_popup_inline_script($session_js);
         }
     }

@@ -539,9 +536,24 @@
         if ( is_feed() || is_admin() || isset($_COOKIE['ma_popup_visitor']) || isset($_GET['elementor-preview']) || ( defined('ELEMENTOR_VERSION') && ElementorPlugin::$instance->preview->is_preview_mode() ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only existence check, no data processed
             return;
         }
-        echo '<script>if (document.cookie.indexOf("ma_popup_visitor=") === -1) {
-            document.cookie = "ma_popup_visitor=1; max-age=31536000; path=/;";
-        }</script>';
+        $this->add_popup_inline_script('if (document.cookie.indexOf("ma_popup_visitor=") === -1) { document.cookie = "ma_popup_visitor=1; max-age=31536000; path=/;"; }');
+    }
+
+    /**
+     * Queue a small inline script through the enqueue API (printed in the footer)
+     * instead of echoing a hardcoded <script> tag. Multiple calls accumulate on the
+     * same inline-only handle.
+     *
+     * @param string $js
+     */
+    private function add_popup_inline_script($js) {
+        if (!wp_script_is('jltma-popup-inline', 'registered')) {
+            wp_register_script('jltma-popup-inline', false, array(), JLTMA_VER, true);
+        }
+        if (!wp_script_is('jltma-popup-inline', 'enqueued')) {
+            wp_enqueue_script('jltma-popup-inline');
+        }
+        wp_add_inline_script('jltma-popup-inline', $js);
     }

     private function get_popup_settings($popup_id) {
--- a/master-addons/inc/admin/settings/settings.php
+++ b/master-addons/inc/admin/settings/settings.php
@@ -114,7 +114,7 @@
         add_action('current_screen', [$this, 'jltma_set_page_title']);
         add_action('admin_enqueue_scripts', [$this, 'jltma_admin_settings_scripts'], 99);
         add_action('admin_body_class', [$this, 'jltma_admin_body_class']);
-        add_action('admin_head', [$this, 'jltma_global_admin_css']);
+        add_action('admin_enqueue_scripts', [$this, 'jltma_global_admin_css']);
         add_action('wp_ajax_jltma_subscribe', [$this, 'handle_subscribe_ajax']);
     }

@@ -151,7 +151,7 @@
      */
     public function jltma_global_admin_css()
     {
-        echo '<style type="text/css">
+        $css = '
             /* Hide separator below Master Addons menu */
             #adminmenu #toplevel_page_master-addons-settings + .wp-menu-separator {
                 display: none !important;
@@ -236,10 +236,10 @@
             .wrap .jltma-cpt-btn-youtube svg {
                 fill: #ff0000;
             }
-        </style>';
+        ';

         // JS to reorder submenu items and add separator/pricing classes
-        echo '<script type="text/javascript">
+        $js = '
             document.addEventListener("DOMContentLoaded", function() {
                 var menu = document.getElementById("toplevel_page_master-addons-settings");
                 if (!menu) return;
@@ -286,7 +286,17 @@
                     }
                 });
             });
-        </script>';
+        ';
+
+        // Load the menu styling and ordering through the core enqueue APIs
+        // (inline-only handles) instead of hardcoded <style>/<script> tags.
+        wp_register_style('jltma-admin-menu', false, array(), JLTMA_VER);
+        wp_enqueue_style('jltma-admin-menu');
+        wp_add_inline_style('jltma-admin-menu', $css);
+
+        wp_register_script('jltma-admin-menu', false, array(), JLTMA_VER, true);
+        wp_enqueue_script('jltma-admin-menu');
+        wp_add_inline_script('jltma-admin-menu', $js);
     }

 	/**
@@ -331,9 +341,9 @@
             remove_action('admin_notices', 'update_nag', 3);
             remove_action('admin_notices', 'maintenance_nag', 10);

-            // Hide any remaining notices via CSS
-            add_action('admin_head', function () {
-                echo '<style type="text/css">
+            // Hide any remaining admin notices on our settings screen. Attached to the
+            // settings stylesheet (enqueued below) instead of a hardcoded <style> tag.
+            $jltma_hide_notices_css = '
                     .notice, .error, .updated, .update-nag, .admin-notice,
                     .jltma-plugin-update-notice, .fs-notice, .fs-slug-master-addons,
                     #wpbody-content > .notice, #wpbody-content > .error, #wpbody-content > .updated,
@@ -345,9 +355,7 @@
                     }
                     #wpfooter {
                         display: none !important;
-                    }
-                </style>';
-            });
+                    }';

             // Dequeue Spectra (UAG) zipwp-images style to prevent CSS conflicts on settings page
             add_action('admin_enqueue_scripts', function () {
@@ -369,6 +377,8 @@
             wp_enqueue_style('jltma-admin-settings');
             wp_enqueue_script('jltma-admin-settings');

+            wp_add_inline_style('jltma-admin-settings', $jltma_hide_notices_css);
+
             // White Label logo & Hidden Nav Menus
             $white_label_settings = jltma_settings()->get('jltma_white_label_settings');
             $white_label_settings = is_array($white_label_settings) ? $white_label_settings : [];
@@ -460,13 +470,9 @@
         remove_all_actions('network_admin_notices');
         remove_all_actions('user_admin_notices');

-        add_action('admin_head', function () {
-            echo '<style type="text/css">
-                #wpfooter {
-                    display: none !important;
-                }
-            </style>';
-        });
+        // Hide the footer for the full-screen wizard (attached to the wizard
+        // stylesheet below instead of a hardcoded <style> tag).
+        $jltma_wizard_css = '#wpfooter { display: none !important; }';

         // Elementor icons for addon card icons
         if (wp_style_is('elementor-icons', 'registered')) {
@@ -478,6 +484,8 @@
         wp_enqueue_style('jltma-setup-wizard');
         wp_enqueue_script('jltma-setup-wizard');

+        wp_add_inline_style('jltma-setup-wizard', $jltma_wizard_css);
+
         // Build recommended plugins data with pre-computed install status.
         $recommended_instance = Recommended_Plugins::get_instance();
         $raw_plugins          = $recommended_instance->plugins_list();
--- a/master-addons/inc/admin/templates/includes/classes/manager.php
+++ b/master-addons/inc/admin/templates/includes/classes/manager.php
@@ -713,7 +713,20 @@
 		public function get_template_data()
 		{

-			$template = $this->get_template_data_array($_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce available for this AJAX handler
+			// Extract and sanitize only the specific fields we need (never forward the whole superglobal).
+			$source = isset( $_REQUEST['source'] ) ? wp_unslash( $_REQUEST['source'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+			if ( is_array( $source ) ) {
+				$source = array_map( 'sanitize_text_field', $source );
+			} else {
+				$source = sanitize_text_field( $source );
+			}
+			$request_data = array(
+				'template_id' => isset( $_REQUEST['template_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['template_id'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+				'tab'         => isset( $_REQUEST['tab'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['tab'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only fetch gated by edit_posts capability in get_template_data_array()
+				'source'      => $source,
+			);
+
+			$template = $this->get_template_data_array($request_data);

 			if (!$template) {
 				wp_send_json_error();
--- a/master-addons/inc/admin/templates/kits/class-importer.php
+++ b/master-addons/inc/admin/templates/kits/class-importer.php
@@ -94,7 +94,7 @@
      */
     public function activate_required_theme()
     {
-        $nonce = $_POST['nonce'];
+        $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';

         if (!wp_verify_nonce($nonce, 'jltma-template-kits-js') || !current_user_can('manage_options')) {
             exit;
@@ -130,9 +130,11 @@
     public function sanitize_svg_on_upload($file)
     {
         if ($file['type'] === 'image/svg+xml') {
-            $file_content = file_get_contents($file['tmp_name']);
+            // Direct file ops on the PHP upload temp path, before WordPress moves it.
+            // WP_Filesystem (ftp/ssh methods) cannot reach the local upload temp file.
+            $file_content = file_get_contents($file['tmp_name']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading the PHP upload temp file for sanitization
             $sanitized_content = $this->sanitize_svg($file_content);
-            file_put_contents($file['tmp_name'], $sanitized_content);
+            file_put_contents($file['tmp_name'], $sanitized_content); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing back the sanitized SVG to the PHP upload temp file before WordPress moves it
         }
         return $file;
     }
@@ -1533,7 +1535,7 @@

             $image_data = wp_remote_retrieve_body($response);
             $tmp = wp_tempnam();
-            file_put_contents($tmp, $image_data);
+            file_put_contents($tmp, $image_data); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing to a wp_tempnam() temp file that media_handle_sideload() then processes

             if (!file_exists($tmp) || filesize($tmp) === 0) {
                 wp_delete_file($tmp);
--- a/master-addons/inc/admin/templates/library/template-library.php
+++ b/master-addons/inc/admin/templates/library/template-library.php
@@ -280,6 +280,25 @@
     /**
      * Get cached kit categories
      */
+    /**
+     * Write a file through the WP_Filesystem API.
+     *
+     * @param string $file
+     * @param string $contents
+     * @return bool
+     */
+    private function fs_put_contents($file, $contents) {
+        global $wp_filesystem;
+        if (empty($wp_filesystem)) {
+            require_once ABSPATH . 'wp-admin/includes/file.php';
+            WP_Filesystem();
+        }
+        if (empty($wp_filesystem)) {
+            return false;
+        }
+        return $wp_filesystem->put_contents($file, $contents, FS_CHMOD_FILE);
+    }
+
     private function get_cached_kit_categories($force_refresh = false) {
         $upload_dir = wp_upload_dir();
         $categories_file = $upload_dir['basedir'] . '/master_addons/templates_kits/kit-categories.json';
@@ -302,7 +321,7 @@
                 $cached = $cache_manager->get_cached_kit_categories($force_refresh);
                 if ($cached !== false) {
                     if (!file_exists($categories_file) && !empty($cached)) {
-                        @file_put_contents($categories_file, json_encode($cached));
+                        $this->fs_put_contents($categories_file, wp_json_encode($cached));
                     }
                     return $cached;
                 }
@@ -331,7 +350,7 @@
                 set_transient($transient_key, $fresh_categories, $cache_expiry);

                 // Save to local file
-                @file_put_contents($categories_file, json_encode($fresh_categories));
+                $this->fs_put_contents($categories_file, wp_json_encode($fresh_categories));

                 return $fresh_categories;
             }
@@ -1328,6 +1347,11 @@
             return;
         }

+        if (!current_user_can('manage_options')) {
+            wp_send_json_error(['message' => 'Insufficient permissions']);
+            return;
+        }
+
         $plugins_with_status = array();

         if( isset($_POST['required_plugins']) && !empty( $_POST['required_plugins'])){
--- a/master-addons/inc/admin/theme-builder/inc/api/select2-api.php
+++ b/master-addons/inc/admin/theme-builder/inc/api/select2-api.php
@@ -14,10 +14,10 @@
      * This is called by WordPress REST API on each request
      */
     public function check_for_permission(){
-        // Allow any logged-in user who can read posts
-        // Using 'read' capability instead of 'edit_posts' to be more permissive
-        // Individual methods will perform their own capability checks as needed
-        return current_user_can('read');
+        // These endpoints feed editor-only post/page/term pickers, so require
+        // the same capability needed to edit content. The REST API validates the
+        // X-WP-Nonce cookie nonce before this callback runs for logged-in users.
+        return current_user_can('edit_posts');
     }


@@ -104,18 +104,10 @@


     public function get_singular_list(){
-        // Only perform security checks if user is logged in
-        if (is_user_logged_in()) {
-            // Verify REST API nonce that comes from JavaScript - be more lenient with nonce check
-            $nonce = $this->request->get_header('X-WP-Nonce');
-            if ($nonce && !wp_verify_nonce($nonce, 'wp_rest')) {
-                // If nonce verification fails, don't block the request
-            }
-
-            // Check if user can read posts
-            if (!current_user_can('read')) {
-                return new WP_Error( 'rest_forbidden', __( 'Insufficient permissions.', 'master-addons' ), [ 'status' => 403 ] );
-            }
+        // Capability is enforced here (and by check_for_permission()); the REST
+        // API has already validated the X-WP-Nonce cookie nonce at this point.
+        if ( ! current_user_can('edit_posts') ) {
+            return new WP_Error( 'rest_forbidden', __( 'Insufficient permissions.', 'master-addons' ), [ 'status' => 403 ] );
         }

         $query_args = [
--- a/master-addons/inc/admin/theme-builder/inc/cpt-hooks.php
+++ b/master-addons/inc/admin/theme-builder/inc/cpt-hooks.php
@@ -272,21 +272,21 @@
                     }
                 } else {
                     // Fallback to post content
-                    echo do_shortcode( $template_post->post_content );
+                    echo wp_kses_post( do_shortcode( $template_post->post_content ) );
                 }
-
+
                 $content = ob_get_clean();
                 return $content;
-
+
             } catch ( Exception $e ) {
                 ob_end_clean();
                 // Return post content as fallback if Elementor fails
-                return do_shortcode( $template_post->post_content );
+                return wp_kses_post( do_shortcode( $template_post->post_content ) );
             }
         }

         // Fallback: Return post content if Elementor is not available
-        return do_shortcode( $template_post->post_content );
+        return wp_kses_post( do_shortcode( $template_post->post_content ) );
     }

     public static function instance()
--- a/master-addons/inc/admin/theme-builder/inc/theme-builder-assets.php
+++ b/master-addons/inc/admin/theme-builder/inc/theme-builder-assets.php
@@ -12,7 +12,7 @@
     public function __construct()
     {

-        add_action('admin_print_scripts', [$this, 'jltma_admin_js']);
+        add_action('admin_enqueue_scripts', [$this, 'jltma_admin_js']);

         // enqueue scripts
         add_action('admin_enqueue_scripts', [$this, 'jltma_header_footer_enqueue_scripts']);
@@ -21,9 +21,11 @@
     // Declare Variable for Rest API
     public function jltma_admin_js()
     {
-        echo "<script type='text/javascript'>n";
-        echo $this->jltma_common_js(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- outputs safe JS built by jltma_common_js()
-        echo "n</script>";
+        // Output the REST var through the enqueue API (inline-only handle) instead of a
+        // hardcoded <script> tag.
+        wp_register_script('jltma-theme-builder-vars', false, array(), JLTMA_VER, false);
+        wp_enqueue_script('jltma-theme-builder-vars');
+        wp_add_inline_script('jltma-theme-builder-vars', $this->jltma_common_js());
     }


--- a/master-addons/inc/admin/widget-builder/class-control-manager.php
+++ b/master-addons/inc/admin/widget-builder/class-control-manager.php
@@ -130,14 +130,27 @@
     private function build_fallback_control($control_key, $field, $type) {
         $label = !empty($field['label']) ? esc_js($field['label']) : 'Control';

+        // Elementor control constants are upper-case (e.g. REPEATER, SELECT). Normalise
+        // the type to a safe constant name so the generated PHP is always valid.
+        $type_const = strtoupper(preg_replace('/[^A-Za-z0-9_]/', '', (string) $type));
+        if ('' === $type_const) {
+            $type_const = 'TEXT';
+        }
+
         $content = "tt$this->add_control(n";
         $content .= "ttt'{$control_key}',n";
         $content .= "ttt[n";
         $content .= "tttt'label' => esc_html__('{$label}', 'master-addons'),n";
-        $content .= "tttt'type' => Controls_Manager::{$type},n";
+        $content .= "tttt'type' => Controls_Manager::{$type_const},n";
+
+        // REPEATER must always carry a 'fields' array, otherwise Elementor's
+        // sanitize_settings() throws a TypeError when iterating null fields.
+        if ('REPEATER' === $type_const) {
+            $content .= "tttt'fields' => array(),n";
+        }

         if (isset($field['default'])) {
-            $default = is_string($field['default']) ? "'" . esc_js($field['default']) . "'" : $field['default'];
+            $default = $this->export_default_value($field['default']);
             $content .= "tttt'default' => {$default},n";
         }

@@ -146,4 +159,29 @@

         return $content;
     }
+
+    /**
+     * Convert a control default value into a valid PHP literal for the generated file.
+     * Arrays (e.g. repeater/group-control defaults) must never be interpolated directly,
+     * which would emit the literal token "Array" and produce a fatal parse error.
+     *
+     * @param mixed $value
+     * @return string
+     */
+    private function export_default_value($value) {
+        if (is_array($value)) {
+            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- generating PHP source, not debug output
+            return var_export($value, true);
+        }
+        if (is_bool($value)) {
+            return $value ? 'true' : 'false';
+        }
+        if (is_int($value) || is_float($value)) {
+            return (string) $value;
+        }
+        if (null === $value) {
+            return "''";
+        }
+        return "'" . esc_js((string) $value) . "'";
+    }
 }
--- a/master-addons/inc/admin/widget-builder/class-rest-controller.php
+++ b/master-addons/inc/admin/widget-builder/class-rest-controller.php
@@ -627,33 +627,25 @@
         $css_code = '';
         $js_code = '';

-        // Sanitize HTML code - allow all HTML/PHP for widget development
+        // Sanitize HTML code. PHP is NEVER allowed (no arbitrary code execution),
+        // regardless of capability. Inline <script> is stripped — JavaScript belongs
+        // in the JS field, which is enqueued separately. Dynamic values use {{placeholders}}.
         if (isset($data['html_code'])) {
-            // For admin users with widget building capability, we allow unfiltered HTML
-            // This is necessary for Elementor widget development
-            if (current_user_can('unfiltered_html')) {
-                $html_code = $data['html_code'];
-            } else {
-                // For other users, use wp_kses_post which allows safe HTML
-                $html_code = wp_kses_post($data['html_code']);
-            }
+            $html_code = $this->strip_php_tags($data['html_code']);
+            $html_code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $html_code);
+            $html_code = preg_replace('#</?scriptb[^>]*>#i', '', $html_code);
         }

-        // Sanitize CSS code - allow CSS but strip PHP/JS tags
+        // Sanitize CSS code - strip PHP/script/style tags and dangerous constructs.
         if (isset($data['css_code'])) {
-            // Remove any potential PHP/script tags from CSS
             $css_code = $this->sanitize_css($data['css_code']);
         }

-        // Sanitize JavaScript code - allow JS but validate syntax
+        // Sanitize JavaScript code. PHP is NEVER allowed; <script> tags are stripped so
+        // the value cannot break out of the generated/enqueued script context.
         if (isset($data['js_code'])) {
-            // For admin users, allow JavaScript for widget development
-            if (current_user_can('unfiltered_html')) {
-                $js_code = $data['js_code'];
-            } else {
-                // For other users, strip tags
-                $js_code = wp_strip_all_tags($data['js_code']);
-            }
+            $js_code = $this->strip_php_tags($data['js_code']);
+            $js_code = preg_replace('#</?scriptb[^>]*>#i', '', $js_code);
         }

         // Also save data in unified format for widget generator
@@ -671,6 +663,23 @@
     }

     /**
+     * Strip every PHP open/close tag (and null bytes) from a string so user input
+     * can never become executable PHP once written into a generated widget file.
+     *
+     * @param string $code
+     * @return string
+     */
+    private function strip_php_tags($code) {
+        if (!is_string($code) || '' === $code) {
+            return '';
+        }
+        $code = str_replace(chr(0), '', $code);
+        $code = preg_replace('/<?php/i', '', $code);
+        $code = str_replace(array('<?=', '<?', '?>'), '', $code);
+        return $code;
+    }
+
+    /**
      * Sanitize CSS code
      *
      * @param string $css
@@ -678,11 +687,19 @@
      */
     private function sanitize_css($css) {
         // Remove any PHP tags
-        $css = preg_replace('/<?php.*??>/s', '', $css);
-        $css = preg_replace('/<?.*??>/s', '', $css);
+        $css = $this->strip_php_tags($css);

-        // Remove any script tags
+        // Remove any <style>/<script> tags so the value cannot break out of the
+        // generated inline <style> block.
+        $css = preg_replace('#</?styleb[^>]*>#i', '', $css);
         $css = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $css);
+        $css = preg_replace('#</?scriptb[^>]*>#i', '', $css);
+
+        // Remove dangerous CSS constructs.
+        $css = preg_replace('/@importb/i', '', $css);
+        $css = preg_replace('/expressions*(/i', '', $css);
+        $css = preg_replace('/(javascript|vbscript)s*:/i', '', $css);
+        $css = preg_replace('/behaviors*:/i', '', $css);

         // Remove any JavaScript event handlers
         $css = preg_replace('/onw+s*=s*["'].*?["']/i', '', $css);
--- a/master-addons/inc/admin/widget-builder/class-widget-admin.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-admin.php
@@ -795,11 +795,13 @@
     }

     /**
-     * Render widget preview with PHP execution
-     * Handles AJAX request to render widget HTML with PHP code executed
+     * Render a static widget preview.
+     * Handles the AJAX request to render widget HTML with mock control values.
+     * No user-supplied PHP or JavaScript is ever executed — placeholders are
+     * replaced with escaped default values and the markup is escaped on output.
      */
     public function render_preview() {
-        // Capability check: only administrators can execute widget previews (contains eval)
+        // Capability check: only administrators can request a widget preview.
         if (!current_user_can('manage_options')) {
             wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
             return;
@@ -811,13 +813,25 @@
             return;
         }

-        $html_code = isset($_POST['html_code']) ? wp_kses_post( wp_unslash( $_POST['html_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
+        $html_code = isset($_POST['html_code']) ? wp_unslash( $_POST['html_code'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above; PHP/script stripped and output escaped below
         $css_code = isset($_POST['css_code']) ? sanitize_textarea_field( wp_unslash( $_POST['css_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
         $controls = isset($_POST['controls']) ? json_decode( sanitize_textarea_field( wp_unslash( $_POST['controls'] ) ), true ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above

-        // Build mock settings array from controls
+        // Preview NEVER executes user code: strip PHP tags and inline <script>.
+        $html_code = str_replace(chr(0), '', $html_code);
+        $html_code = preg_replace('/<?php/i', '', $html_code);
+        $html_code = str_replace(array('<?=', '<?', '?>'), '', $html_code);
+        $html_code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $html_code);
+        $html_code = preg_replace('#</?scriptb[^>]*>#i', '', $html_code);
+
+        // Strip </style> and PHP from preview CSS so it cannot break out of <style>.
+        $css_code = preg_replace('#</?styleb[^>]*>#i', '', $css_code);
+        $css_code = preg_replace('/<?php/i', '', $css_code);
+        $css_code = str_replace(array('<?=', '<?', '?>'), '', $css_code);
+
+        // Build mock settings array from control defaults.
         $settings = [];
-        if (!empty($controls)) {
+        if (!empty($controls) && is_array($controls)) {
             foreach ($controls as $control) {
                 if (isset($control['name']) && isset($control['default'])) {
                     $settings[$control['name']] = $control['default'];
@@ -825,86 +839,25 @@
             }
         }

-        // Replace placeholders with PHP variables using regex to handle dot notation
-        // Handles: {{field}}, {{field.property}}, {{field.property.subproperty}}
-        if (!empty($controls)) {
-            foreach ($controls as $control) {
-                if (isset($control['name'])) {
-                    $control_name = $control['name'];
-
-                    // Regex to match {{control_name}} or {{control_name.property}} or {{control_name.property.subproperty}}
-                    // Pattern: {{control_name}} followed by optional .property.property...
-                    $pattern = '/{{' . preg_quote($control_name, '/') . '((?:.[a-zA-Z0-9_]+)*)}}/';
-
-                    $html_code = preg_replace_callback($pattern, function($matches) use ($control_name) {
-                        $properties = $matches[1]; // e.g., "" or ".tabs" or ".property.subproperty"
-
-                        if (empty($properties)) {
-                            // No properties, just {{control_name}}
-                            // Return: $settings['control_name']
-                            return "$settings['" . $control_name . "']";
-                        } else {
-                            // Has properties like .tabs or .property.subproperty
-                            // Convert to: $settings['control_name']['tabs'] or $settings['control_name']['property']['subproperty']
-                            $props = explode('.', ltrim($properties, '.'));
-                            $result = "$settings['" . $control_name . "']";
-                            foreach ($props as $prop) {
-                                if (!empty($prop)) {
-                                    $result .= "['" . $prop . "']";
-                                }
-                            }
-                            return $result;
-                        }
-                    }, $html_code);
+        // Substitute {{field}} / {{field.prop}} placeholders with the mock default
+        // VALUE (escaped). This is a static render — no PHP is evaluated.
+        $html_code = preg_replace_callback('/{{([^}]+)}}/', function($matches) use ($settings) {
+            $path = array_filter(array_map('trim', explode('.', trim($matches[1]))), 'strlen');
+            $value = $settings;
+            foreach ($path as $key) {
+                if (is_array($value) && isset($value[$key])) {
+                    $value = $value[$key];
+                } else {
+                    return '';
                 }
             }
-        }
-
-        // Execute PHP code in the HTML
-        ob_start();
-
-        // Make settings available in the PHP context
-        extract($settings, EXTR_SKIP);
-
-        // Render the code
-        if (strpos($html_code, '<?php') === false && strpos($html_code, '<?=') === false) {
-            // No PHP code, just output as is
-            echo wp_kses_post( $html_code );
-        } else {
-            // Has PHP code: write it to a temporary file in the uploads directory
-            // and include it. include is used instead of eval(), which is not
-            // permitted in WordPress.org hosted plugins.
-            $upload_dir = wp_upload_dir();
-            $tmp_dir    = trailingslashit($upload_dir['basedir']) . 'master-addons/widget-builder/tmp/';
-
-            if (!file_exists($tmp_dir)) {
-                wp_mkdir_p($tmp_dir);
-                file_put_contents($tmp_dir . 'index.php', "<?phpn// Silence is golden.n");
-            }
-
-            $tmp_file = $tmp_dir . 'preview-' . wp_generate_password(20, false) . '.php';
-
-            if (false !== file_put_contents($tmp_file, $html_code)) {
-                try {
-                    include $tmp_file;
-                } catch (ParseError $e) {
-                    echo '<div style="color:red;padding:20px;background:#fff3cd;border:1px solid #ffc107;">';
-                    echo '<h3>PHP Parse Error in Preview:</h3>';
-                    echo '<pre>' . esc_html($e->getMessage()) . '</pre>';
-                    echo '<h4>Line ' . absint( $e->getLine() ) . '</h4>';
-                    echo '</div>';
-                } catch (Exception $e) {
-                    echo '<div style="color:red;padding:20px;background:#fff3cd;border:1px solid #ffc107;">';
-                    echo '<h3>PHP Error in Preview:</h3>';
-                    echo '<pre>' . esc_html($e->getMessage()) . '</pre>';
-                    echo '</div>';
-                } finally {
-                    wp_delete_file($tmp_file);
-                }
+            if (is_array($value)) {
+                $value = isset($value['url']) ? $value['url'] : '';
             }
-        }
+            return esc_html((string) $value);
+        }, $html_code);

-        $rendered_html = ob_get_clean();
+        $rendered_html = wp_kses_post($html_code);

         // Build full HTML document with CSS
         $output = '<!DOCTYPE html>
--- a/master-addons/inc/admin/widget-builder/class-widget-builder-init.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-builder-init.php
@@ -26,6 +26,7 @@
     private function __construct() {
         add_action('init', [$this, 'initialize'], 1);
         add_action('admin_init', [$this, 'admin_redirects']);
+        add_action('admin_init', [$this, 'maybe_migrate']);
     }

     public function initialize() {
@@ -146,4 +147,135 @@
         }
         // phpcs:enable WordPress.Security.NonceVerification.Recommended
     }
+
+    /**
+     * One-time migration. Re-sanitizes stored widget code (strips any PHP/script that
+     * older versions may have persisted), purges stale generated files from current and
+     * legacy locations, and regenerates every widget from the now-clean data. Existing
+     * widget posts are never deleted — only their data is scrubbed and files rebuilt.
+     * Runs once per version (gated by an option) inside an authorised admin request.
+     */
+    public function maybe_migrate() {
+        $option_key = 'jltma_widget_builder_migrated';
+        $version    = '3.1.1';
+
+        if (get_option($option_key) === $version) {
+            return;
+        }
+
+        if (!current_user_can('manage_options')) {
+            return;
+        }
+
+        // 1) Remove stale generated trees (current + legacy locations).
+        $upload = wp_upload_dir();
+        $base   = trailingslashit($upload['basedir']);
+        $this->delete_directory($base . 'master_addons/widgets');
+        $this->delete_directory($base . 'master-addons/widget-builder/generated');
+        $this->delete_directory($base . 'master-addons/widget-builder/tmp');
+
+        // 2) Scrub persisted widget data, then regenerate files from clean data.
+        $widget_ids = get_posts([
+            'post_type'      => 'jltma_widget',
+            'post_status'    => 'any',
+            'posts_per_page' => -1,
+            'fields'         => 'ids',
+        ]);
+
+        foreach ($widget_ids as $widget_id) {
+            $data = get_post_meta($widget_id, '_jltma_widget_data', true);
+            if (is_array($data)) {
+                if (isset($data['html_code'])) {
+                    $data['html_code'] = $this->scrub_html($data['html_code']);
+                }
+                if (isset($data['css_code'])) {
+                    $data['css_code'] = $this->scrub_css($data['css_code']);
+                }
+                if (isset($data['js_code'])) {
+                    $data['js_code'] = $this->scrub_js($data['js_code']);
+                }
+                update_post_meta($widget_id, '_jltma_widget_data', $data);
+            }
+
+            $generator = new Widget_Generator($widget_id);
+            $generator->generate();
+        }
+
+        // 3) Drop the orphaned option left by the old option-based builder.
+        delete_option('jltma_custom_widgets');
+
+        update_option($option_key, $version);
+    }
+
+    /**
+     * Strip PHP tags and inline <script> from widget HTML.
+     *
+     * @param string $code
+     * @return string
+     */
+    private function scrub_html($code) {
+        if (!is_string($code) || '' === $code) {
+            return '';
+        }
+        $code = str_replace(chr(0), '', $code);
+        $code = preg_replace('/<?php/i', '', $code);
+        $code = str_replace(['<?=', '<?', '?>'], '', $code);
+        $code = preg_replace('#<scriptb[^>]*>.*?</script>#is', '', $code);
+        $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+        return $code;
+    }
+
+    /**
+     * Strip PHP tags and <style>/<script> tags from widget CSS.
+     *
+     * @param string $code
+     * @return string
+     */
+    private function scrub_css($code) {
+        if (!is_string($code) || '' === $code) {
+            return '';
+        }
+        $code = str_replace(chr(0), '', $code);
+        $code = preg_replace('/<?php/i', '', $code);
+        $code = str_replace(['<?=', '<?', '?>'], '', $code);
+        $code = preg_replace('#</?styleb[^>]*>#i', '', $code);
+        $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+        return $code;
+    }
+
+    /**
+     * Strip PHP tags and <script> tags from widget JS.
+     *
+     * @param string $code
+     * @return string
+     */
+    private function scrub_js($code) {
+        if (!is_string($code) || '' === $code) {
+            return '';
+        }
+        $code = str_replace(chr(0), '', $code);
+        $code = preg_replace('/<?php/i', '', $code);
+        $code = str_replace(['<?=', '<?', '?>'], '', $code);
+        $code = preg_replace('#</?scriptb[^>]*>#i', '', $code);
+        return $code;
+    }
+
+    /**
+     * Recursively delete a directory via WP_Filesystem.
+     *
+     * @param string $dir
+     */
+    private function delete_directory($dir) {
+        if (empty($dir) || !is_dir($dir)) {
+            return;
+        }
+        global $wp_filesystem;
+        if (empty($wp_filesystem)) {
+            require_once ABSPATH . 'wp-admin/includes/file.php';
+            WP_Filesystem();
+        }
+        if (!empty($wp_filesystem)) {
+            $wp_filesystem->delete($dir, true);
+        }
+    }
 }
--- a/master-addons/inc/admin/widget-builder/class-widget-generator.php
+++ b/master-addons/inc/admin/widget-builder/class-widget-generator.php
@@ -154,12 +154,43 @@
      */
     private function create_directory() {
         if (!file_exists($this->widget_dir)) {
-            return wp_mkdir_p($this->widget_dir);
+            if (!wp_mkdir_p($this->widget_dir)) {
+                return false;
+            }
         }
+
+        // Defense in depth: drop a silence index.php into the base and per-widget
+        // directories so generated files cannot be listed/browsed directly. Generated
+        // PHP is plugin-authored and ABSPATH-guarded, so it is inert over the web.
+        $this->harden_directory($this->upload_dir);
+        $this->harden_directory($this->widget_dir);
+
         return true;
     }

     /**
+     * Write a silence index.php into a generated directory.
+     *
+     * @param string $dir
+     */
+    private function harden_directory($dir) {
+        if (empty($dir)) {
+            return;
+        }
+
+        global $wp_filesystem;
+        if (empty($wp_filesystem)) {
+            require_once(ABSPATH . '/wp-admin/includes/file.php');
+            WP_Filesystem();
+        }
+
+        $index = trailingslashit($dir) . 'index.php';
+        if (!file_exists($index)) {
+            $wp_filesystem->put_contents($index, "<?phpn// Silence is golden.n", FS_CHMOD_FILE);
+        }
+    }
+
+    /**
      * Generate PHP widget file
      *
      * @return bool|WP_Error
@@ -397,6 +428,8 @@
      */
     private function build_get_icon() {
         $icon = !empty($this->widget_data['icon']) ? $this->widget_data['icon'] : 'eicon-code';
+        // Escape for safe interpolation into a single-quoted PHP string literal.
+        $icon = addslashes(sanitize_text_field($icon));

         $content = "tpublic function get_icon() {n";
         $content .= "ttreturn '{$icon}';n";
@@ -412,6 +445,8 @@
      */
     private function build_get_categories() {
         $category = !empty($this->widget_data['category']) ? $this->widget_data['category'] : 'master-addons';
+        // Escape for safe interpolation into a single-quoted PHP string literal.
+        $category = addslashes(sanitize_text_field($category));

         $content = "tpublic function get_categories() {n";
         $content .= "ttreturn ['{$category}'];n";
@@ -1020,7 +1055,7 @@

                     // For simple variable references, wrap in echo with appropriate escaping
                     if (in_array(strtolower($control_type), ['wysiwyg', 'code'])) {
-                        return '<' . '?php echo ' . $var_ref . '; ?' . '>';
+                        return '<' . '?php echo wp_kses_post(' . $var_ref . '); ?' . '>';
                     } else {
                         return '<' . '?php echo esc_html(' . $var_ref . '); ?' . '>';
                     }
@@ -1251,12 +1286,13 @@
             return '';
         }

-        // Ensure the HTML doesn't contain closing PHP tags that would break the context
-        // This is a security measure to prevent breaking out of the render method
-        // We replace any standalone PHP tag boundaries
-        $html = str_replace('?' . '><' . '?php', '<!-- php-boundary -->', $html);
-
-        // Ensure the html does not contain {{  }} template string
+        // Security: strip every PHP open/close tag so user-supplied markup can never
+        // execute as PHP once written into the generated widget file. Generated files
+        // contain only plugin-authored PHP; user HTML is treated as inert markup whose
+        // {{placeholders}} are converted to escaped echo statements elsewhere.
+        $html = str_replace(chr(0), '', $html);
+        $html = preg_replace('/<?php/i', '', $html);
+        $html = str_replace(array('<?=', '<?', '?>'), '', $html);

         return $html;
     }
@@ -1419,9 +1455,9 @@

         $css = trim($css);

-        // Remove any PHP tags
-        $css = preg_replace('/<?php.*??>/s', '', $css);
-        $css = preg_replace('/<?.*??>/s', '', $css);
+        // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
+        $css = preg_replace('/<?php/i', '', $css);
+        $css = str_replace(array('<?=', '<?', '?>'), '', $css);

         // Remove any HTML script tags
         $css = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $css);
@@ -1452,16 +1488,17 @@

         $js = trim($js);

-        // Remove any PHP tags
-        $js = preg_replace('/<?php.*??>/s', '', $js);
-        $js = preg_replace('/<?.*??>/s', '', $js);
+        // Remove any PHP tags (balanced and bare) so nothing executes as PHP.
+        $js = preg_replace('/<?php/i', '', $js);
+        $js = str_replace(array('<?=', '<?', '?>'), '', $js);
+
+        // Strip <script> tags so the value cannot break out of the enqueued/inline
+        // <script> context. JS string literals should not contain literal script tags.
+        $js = preg_replace('#</?scriptb[^>]*>#i', '', $js);

         // Remove null bytes
         $js = str_replace(chr(0), '', $js);

-        // Note: We don't strip HTML tags from JS as they might be part of string literals
-        // The responsibility is on the admin user to write secure code
-
         return $js;
     }

--- a/master-addons/inc/admin/widget-builder/widget-builder.php
+++ b/master-addons/inc/admin/widget-builder/widget-builder.php
@@ -1,714 +0,0 @@
-<?php
-
-namespace MasterAddonsIncAdminWidgetBuilder;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly.
-}
-
-use MasterAddonsIncClassesExtension_Prototype;
-
-/**
- * Master Addons Widget Builder
- * No-code custom widget creation for Elementor
- */
-
-class Widget_Builder extends Extension_Prototype {
-
-    public static $instance = null;
-
-    public function __construct() {
-        // NOTE: admin_menu is now handled by JLTMA_Widget_Admin class
-        // add_action('admin_menu', array($this, 'add_widget_builder_menu'));
-
-        // NOTE: admin_enqueue_scripts is now handled by JLTMA_Widget_Admin class
-        // add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
-
-        add_action('wp_ajax_jltma_save_custom_widget', array($this, 'save_custom_widget'));
-        add_action('wp_ajax_jltma_delete_custom_widget', array($this, 'delete_custom_widget'));
-        add_action('wp_ajax_jltma_get_custom_widgets', array($this, 'get_custom_widgets'));
-        add_action('wp_ajax_jltma_get_widget_fields', array($this, 'get_widget_fields'));
-        add_action('elementor/widgets/widgets_registered', array($this, 'register_custom_widgets'));
-        add_action('wp_enqueue_scripts', array($this, 'enqueue_widget_styles'));
-    }
-
-    /**
-     * Add Widget Builder submenu under Master Addons
-     * NOTE: Menu is now handled by JLTMA_Widget_Admin class
-     */
-    public function add_widget_builder_menu() {
-        // Menu registration moved to JLTMA_Widget_Admin class
-        // This method kept for backward compatibility but does nothing
-    }
-
-    /**
-     * Enqueue admin scripts and styles
-     */
-    public function admin_enqueue_scripts($hook) {
-        // Only load on Widget Builder page
-        if ($hook !== 'master-addons_page_jltma-widget-builder') {
-            return;
-        }
-
-        // Enqueue Widget Builder React app
-        wp_enqueue_script(
-            'jltma-widget-builder-app',
-            JLTMA_URL . '/assets/js/admin/widget-builder-app.js',
-            array('wp-element', 'wp-i18n'),
-            JLTMA_VER,
-            true
-        );
-
-        // Enqueue Widget Builder styles
-        wp_enqueue_style(
-            'jltma-widget-builder',
-            JLTMA_URL . '/assets/css/admin/widget-builder.css',
-            array(),
-            JLTMA_VER
-        );
-
-        // Localize script data
-        $localize_data = array(
-            'ajaxurl' => admin_url('admin-ajax.php'),
-            'nonce' => wp_create_nonce('jltma_widget_builder_nonce'),
-            'pluginUrl' => JLTMA_URL,
-            'strings' => array(
-                'widgetBuilder' => __('Widget Builder', 'master-addons'),
-                'createWidget' => __('Create New Widget', 'master-addons'),
-                'editWidget' => __('Edit Widget', 'master-addons'),
-                'deleteWidget' => __('Delete Widget', 'master-addons'),
-                'saveWidget' => __('Save Widget', 'master-addons'),
-                'widgetName' => __('Widget Name', 'master-addons'),
-                'widgetTitle' => __('Widget Title', 'master-addons'),
-                'widgetIcon' => __('Widget Icon', 'master-addons'),
-                'widgetCategory' => __('Widget Category', 'master-addons'),
-                'addField' => __('Add Field', 'master-addons'),
-                'fieldType' => __('Field Type', 'master-addons'),
-                'fieldLabel' => __('Field Label', 'master-addons'),
-                'fieldName' => __('Field Name', 'master-addons'),
-                'preview' => __('Preview', 'master-addons'),
-                'livePreview' => __('Live Preview', 'master-addons'),
-                'widgetCode' => __('Widget Code', 'master-addons'),
-                'exportWidget' => __('Export Widget', 'master-addons'),
-                'importWidget' => __('Import Widget', 'master-addons'),
-                'noCodeBuilder' => __('No-Code Widget Builder', 'master-addons'),
-                'dragDropInterface' => __('Drag & Drop Interface', 'master-addons'),
-                'unlimitedWidgets' => __('Create Unlimited Custom Widgets', 'master-addons'),
-                'searchWidgets' => __('Search widgets...', 'master-addons'),
-                'filterByCategory' => __('Filter by Category', 'master-addons'),
-                'allCategories' => __('All Categories', 'master-addons'),
-                'basic' => __('Basic', 'master-addons'),
-                'advanced' => __('Advanced', 'master-addons'),
-                'ecommerce' => __('eCommerce', 'master-addons'),
-                'forms' => __('Forms', 'master-addons'),
-                'media' => __('Media', 'master-addons'),
-                'fieldTypes' => array(
-                    'text' => __('Text Input', 'master-addons'),
-                    'textarea' => __('Textarea', 'master-addons'),
-                    'wysiwyg' => __('WYSIWYG Editor', 'master-addons'),
-                    'select' => __('Select Dropdown', 'master-addons'),
-                    'choose' => __('Choose Control', 'master-addons'),
-                    'color' => __('Color Picker', 'master-addons'),
-                    'media' => __('Media Upload', 'master-addons'),
-                    'gallery' => __('Gallery', 'master-addons'),
-                    'icon' => __('Icon Picker', 'master-addons'),
-                    'url' => __('URL Input', 'master-addons'),
-                    'number' => __('Number Input', 'master-addons'),
-                    'slider' => __('Range Slider', 'master-addons'),
-                    'date_time' => __('Date Time Picker', 'master-addons'),
-                    'switcher' => __('Switcher Toggle', 'master-addons'),
-                    'border' => __('Border Control', 'master-addons'),
-                    'typography' => __('Typography', 'master-addons'),
-                    'background' => __('Background', 'master-addons'),
-                    'box_shadow' => __('Box Shadow', 'master-addons'),
-                    'text_shadow' => __('Text Shadow', 'master-addons'),
-                    'repeater' => __('Repeater Fields', 'master-addons')
-                )
-            ),
-        );
-
-        wp_localize_script('jltma-widget-builder-app', 'JLTMAWidgetBuilder', $localize_data);
-    }
-
-    /**
-     * Render Widget Builder page
-     * NOTE: This is now handled by JLTMA_Widget_Admin class
-     * When widget_id is present, it loads React editor
-     * When no widget_id, it shows list table (handled by JLTMA_Widget_Admin)
-     */
-    public function widget_builder_page() {
-        // Check if we're editing a specific widget
-        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query var that selects which widget the admin page renders; not form processing.
-        $widget_id = isset($_GET['widget_id']) ? intval($_GET['widget_id']) : 0;
-
-        if ($widget_id) {
-            // Render the Widget Builder React app editor
-            ?>
-            <div class="wrap jltma-widget-builder-wrap">
-                <div id="jltma-widget-builder-app"></div>
-            </div>
-            <?php
-        } else {
-            // List table is handled by JLTMA_Widget_Admin class
-            // This ensures backward compatibility
-            ?>
-            <div class="wrap jltma-widget-builder-wrap">
-                <div id="jltma-widget-builder-app"></div>
-            </div>
-            <?php
-        }
-    }
-
-
-    /**
-     * AJAX: Save custom widget
-     */
-    public function save_custom_widget() {
-        check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
-        if (!current_user_can('manage_options')) {
-            wp_send_json_error(__('Insufficient permissions', 'master-addons'));
-            return;
-        }
-
-        $widget_data = isset($_POST['widget_data']) ? json_decode( wp_unslash( $_POST['widget_data'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded and sanitized via sanitize_widget_data() below
-
-        if (empty($widget_data['name']) || empty($widget_data['title'])) {
-            wp_send_json_error(__('Widget name and title are required', 'master-addons'));
-            return;
-        }
-
-        // Sanitize widget data
-        $widget_data = $this->sanitize_widget_data($widget_data);
-
-        // Validate widget name format
-        if (!preg_match('/^[a-z0-9_-]+$/', $widget_data['name'])) {
-            wp_send_json_error(__('Widget name must contain only lowercase letters, numbers, hyphens and underscores', 'master-addons'));
-            return;
-        }
-
-        // Save to database
-        $widgets = get_option('jltma_custom_widgets', array());
-        $widget_id = isset($widget_data['id']) ? $widget_data['id'] : uniqid('jltma_widget_');
-        $widget_data['id'] = $widget_id;
-        $widgets[$widget_id] = $widget_data;
-
-        if (update_option('jltma_custom_widgets', $widgets)) {
-            // Generate widget class file
-            if ($this->generate_widget_class($widget_data)) {
-                wp_send_json_success(array(
-                    'message' => __('Widget saved successfully', 'master-addons'),
-                    'widget_id' => $widget_id
-                ));
-            } else {
-                wp_send_json_error(__('Widget saved but failed to generate class file', 'master-addons'));
-            }
-        } else {
-            wp_send_json_error(__('Failed to save widget', 'master-addons'));
-        }
-    }
-
-    /**
-     * AJAX: Delete custom widget
-     */
-    public function delete_custom_widget() {
-        check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
-        if (!current_user_can('manage_options')) {
-            wp_send_json_error(__('Insufficient permissions', 'master-addons'));
-        }
-
-        $widget_id = isset( $_POST['widget_id'] ) ? sanitize_text_field( wp_unslash( $_POST['widget_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above via check_ajax_referer
-        $widgets = get_option('jltma_custom_widgets', array());
-
-        if (isset($widgets[$widget_id])) {
-            unset($widgets[$widget_id]);
-            update_option('jltma_custom_widgets', $widgets);
-
-            // Delete widget class file
-            $this->delete_widget_class($widget_id);
-
-            wp_send_json_success(__('Widget deleted successfully', 'master-addons'));
-        } else {
-            wp_send_json_error(__('Widget not found', 'master-addons'));
-        }
-    }
-
-    /**
-     * AJAX: Get custom widgets
-     */
-    public function get_custom_widgets() {
-        check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
-        $widgets = get_option('jltma_custom_widgets', array());
-        wp_send_json_success($widgets);
-    }
-
-    /**
-     * AJAX: Get widget field types
-     */
-    public function get_widget_fields() {
-        check_ajax_referer('jltma_widget_builder_nonce', 'nonce');
-
-        wp_send_json_success($this->get_available_field_types());
-    }
-
-    /**
-     * Register custom widgets with Elementor
-     */
-    public function register_custom_widgets() {
-        if (!class_exists('\Elementor\Plugin')) {
-            return;
-        }
-
-        // Register custom categories first
-        $this->register_custom_categories();
-
-        $widgets = get_option('jltma_custom_widgets', array());
-
-
-        if (empty($widgets) || !is_array($widgets)) {
-            return;
-        }
-
-        foreach ($widgets as $widget_id => $widget_data) {
-            if (is_array($widget_data) && !empty($widget_data['name'])) {
-                $this->register_single_widget($widget_id, $widget_data);
-            }
-        }
-    }
-
-    /**
-     * Register custom Elementor categories
-     */
-    private function register_custom_categories() {
-        if (!class_exists('\Elementor\Plugin')) {
-            return;
-        }
-
-        // Get custom categories from options
-        $custom_categories = get_option('jltma_custom_widget_categories', array());
-
-        if (empty($custom_categories) || !is_array($custom_categories)) {
-            return;
-        }
-
-        $elements_manager = ElementorPlugin::$instance->elements_manager;
-
-        // Register each custom category
-        foreach ($custom_categories as $slug => $title) {
-            // Check if category doesn't already exist
-            $existing_categories = $elements_manager->get_categories();
-
-            if (!isset($existing_categories[$slug])) {
-                $elements_manager->add_category(
-                    $slug,
-                    array(
-                        'title' => $title,
-                        'icon' => 'eicon-posts-ticker',
-                    )
-                );
-            }
-        }
-    }
-
-    /**
-    

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-2026-9281
# Blocks exploitation of the Stored XSS via jtlma_custom_js parameter in Elementor AJAX save
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2026-9281 Master Addons Custom JS XSS via Elementor AJAX',severity:'CRITICAL',tag:'CVE-2026-9281'"
  SecRule ARGS_POST:action "@streq elementor_ajax" "chain"
    SecRule ARGS_POST:jtlma_custom_js "@rx <script|javascript:|onw+=|alert(|document.cookie|eval(|fromCharCode" "t:none"

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