CVE-2026-2358

WP ULike <= 5.0.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attribute

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
5.0.2
Patched in
37d
Time to patch

Description

The WP ULike plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `[wp_ulike_likers_box]` shortcode `template` attribute in all versions up to, and including, 5.0.1. This is due to the use of `html_entity_decode()` on shortcode attributes without subsequent output sanitization, which effectively bypasses WordPress's `wp_kses_post()` content filtering. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. The post must have at least one like for the XSS to render.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.0.1
PublishedMarch 10, 2026
Last updatedApril 16, 2026
Affected pluginwp-ulike

What Changed in the Fix

Changes introduced in v5.0.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to exploit **CVE-2026-2358**, a Stored Cross-Site Scripting (XSS) vulnerability in the WP ULike plugin. ### 1. Vulnerability Summary The `[wp_ulike_likers_box]` shortcode in WP ULike <= 5.0.1 processes the `template` attribute using `html_entity_decode…

Show full research plan

This research plan outlines the steps required to exploit CVE-2026-2358, a Stored Cross-Site Scripting (XSS) vulnerability in the WP ULike plugin.

1. Vulnerability Summary

The [wp_ulike_likers_box] shortcode in WP ULike <= 5.0.1 processes the template attribute using html_entity_decode() but fails to sanitize the output before rendering it. This allows an attacker with Contributor-level permissions (who can create posts and use shortcodes) to inject malicious HTML/JavaScript. By encoding the payload into HTML entities within the shortcode attribute, the attacker bypasses WordPress's standard content filtering (wp_kses_post), which is usually applied when saving a post.

2. Attack Vector Analysis

  • Vulnerable Shortcode: [wp_ulike_likers_box]
  • Vulnerable Attribute: template
  • Authentication Level: Contributor or higher.
  • Required Action: Saving a post/page containing the malicious shortcode.
  • Precondition: The post containing the shortcode must have at least one "like" recorded in the database for the "likers box" (and thus the XSS payload) to render.

3. Code Flow (Inferred)

  1. Entry Point: A user with Contributor+ permissions saves a post containing:
    [wp_ulike_likers_box template="&lt;script&gt;alert(1)&lt;/script&gt;"]
  2. Shortcode Registration: The plugin registers the shortcode (likely via add_shortcode( 'wp_ulike_likers_box', ... )).
  3. Shortcode Execution: When the post is viewed, WordPress calls the plugin's callback function for this shortcode.
  4. Insecure Processing: The callback function extracts the template attribute and applies html_entity_decode($atts['template']).
  5. Sink: The decoded string (now containing raw <script> tags) is returned by the shortcode and echoed into the page HTML without further sanitization.

4. Nonce Acquisition Strategy

While creating the post does not require a plugin-specific nonce, triggering the XSS requires the post to have at least one like. To automate this via the wp_ulike AJAX action, a frontend nonce is required.

  1. Identify Shortcode Page: The wp_ulike scripts and nonces are typically enqueued on pages where a like button exists.
  2. Create Trigger Post: Use WP-CLI to create a post containing the plugin's default like button and the malicious shortcode.
  3. Navigate and Extract:
    • Navigate to the post URL using browser_navigate.
    • Use browser_eval to extract the localized nonce.
    • Inferred JS Variable: window.wp_ulike_params?.nonce (Verify this via browser_eval("window.wp_ulike_params")).

5. Exploitation Strategy

This exploit requires two phases: Injection and Triggering.

Phase 1: Injection (Contributor)

  1. Log in as a Contributor.
  2. Create a new post with a payload encoded in the template attribute.
    • Payload: [wp_ulike_likers_box template="&lt;img src=x onerror=alert(document.domain)&gt;"]
    • Note: We also need a standard like button on the page to facilitate "liking" it.
    • Shortcode Body: [wp_ulike] [wp_ulike_likers_box template="&lt;img src=x onerror=alert(document.domain)&gt;"]

Phase 2: Triggering (Subscriber or Anon)

  1. Navigate to the newly created post.
  2. Identify the ID of the post.
  3. Extract the wp_ulike nonce from the page source.
  4. Perform an AJAX request to "like" the post.
    • Action: wp_ulike
    • Endpoint: /wp-admin/admin-ajax.php
    • Parameters:
      • action: wp_ulike
      • id: [Post ID]
      • nonce: [Extracted Nonce]
      • type: post
      • status: 1

6. Test Data Setup

  1. Users:
    • Create a user contributor_user with the contributor role.
  2. Plugin Configuration: Ensure "Like" functionality is enabled for posts (default behavior).
  3. The Post:
    wp post create --post_type=post --post_status=publish --post_author=$(wp user get contributor_user --field=ID) --post_title="XSS Test" --post_content='[wp_ulike] [wp_ulike_likers_box template="&lt;img src=x onerror=alert(document.domain)&gt;"]'
    

7. Expected Results

  1. When Phase 2 (the Like) is completed, the database table wp_ulike (or similar) will contain an entry for the post ID.
  2. When any user (including Admin) visits the post, the plugin will render the likers box.
  3. The template attribute will be decoded from &lt;img ... &gt; to <img ... >.
  4. The browser will attempt to load the image with source x, fail, and execute the onerror handler, triggering the alert(document.domain).

8. Verification Steps

  1. Check Database for Like:
    wp db query "SELECT * FROM wp_ulike WHERE item_id = [POST_ID]"
    
  2. Verify Raw Output:
    Use http_request to GET the post URL and check if the unescaped payload appears in the response:
    # Look for the decoded HTML in the response
    grep "<img src=x onerror=alert(document.domain)>"
    

9. Alternative Approaches

If html_entity_decode is not the only transformation, try double-encoding or different HTML entities:

  • Decimal Entities: &#60;script&#62;alert(1)&#60;/script&#62;
  • Hex Entities: &#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;
  • Different Sink Context: If the template attribute is rendered inside a specific container, try breaking out of attributes: template='"><script>alert(1)</script>'. (Since the bug specifically cites html_entity_decode bypassing wp_kses_post, the entity approach is most likely).
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP ULike plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) via the `[wp_ulike_likers_box]` shortcode's `template` attribute. This occurs because the plugin uses `html_entity_decode()` on attribute values without subsequent sanitization, allowing authenticated attackers with Contributor-level permissions to bypass WordPress's default `wp_kses_post()` filters.

Vulnerable Code

// includes/hooks/shortcodes.php line 208
if( ! empty( $args['template']  ) ){
    $args['template'] = html_entity_decode( $args['template']  );
}

Security Fix

Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: about-hero.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: api-old.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: banner-pro.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: checkmark.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: col-doc.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: col-review.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: col-support.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: cross-remove.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: database-old.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: database.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: group.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: hero.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: icon.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: rocket.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: seo.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: statistics.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: table.svg
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/img/svg: text.svg
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/js/wp-ulike.js /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/assets/js/wp-ulike.js
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/assets/js/wp-ulike.js	2026-02-09 16:07:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/assets/js/wp-ulike.js	2026-03-05 09:37:42.000000000 +0000
@@ -1,4 +1,4 @@
-/*! WP ULike - v5.0.1
+/*! WP ULike - v5.0.2
  *  https://wpulike.com
  *  TechnoWich 2026;
  */
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/includes/classes/class-wp-ulike-purge-cache.php /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/includes/classes/class-wp-ulike-purge-cache.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/includes/classes/class-wp-ulike-purge-cache.php	2026-02-01 09:28:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/includes/classes/class-wp-ulike-purge-cache.php	2026-03-05 09:37:42.000000000 +0000
@@ -23,6 +23,7 @@
 		{
 			$this->purgeEnduranceCache();
 			$this->purgeHummingbirdCache();
+			$this->purgeKinstaCache();
 			$this->purgeLitespeedCache();
 			$this->purgeSiteGroundCache();
 			$this->purgeSwiftPerformanceCache();
@@ -42,6 +43,7 @@
 			if (!empty($post_ids)) {
 				$this->purgeEnduranceCache($post_ids, $reffer_url);
 				$this->purgeHummingbirdCache($post_ids, $reffer_url);
+				$this->purgeKinstaCache($post_ids, $reffer_url);
 				$this->purgeLitespeedCache($post_ids, $reffer_url);
 				$this->purgeSiteGroundCache($post_ids, $reffer_url);
 				$this->purgeSwiftPerformanceCache($post_ids, $reffer_url);
@@ -56,6 +58,58 @@
 			}
 		}
 
+		/**
+		 * Kinsta cache purge. Only runs when Kinsta MU plugin is present; uses kinsta_cache_purge->initiate_purge( $post_id ).
+		 * Also purges referrer URL via GET {url}/kinsta-clear-cache/ when it differs from the post.
+		 *
+		 * @param array       $post_ids   Post IDs to purge.
+		 * @param string|null $reffer_url Referrer URL to purge when different from post.
+		 * @see https://kinsta.com/
+		 */
+		protected function purgeKinstaCache( $post_ids = [], $reffer_url = null ) {
+			global $kinsta_muplugin;
+
+			if ( ! did_action( 'init' ) || ! isset( $kinsta_muplugin ) || ! class_exists( '\\Kinsta\\KMP' ) || ! $kinsta_muplugin instanceof \\Kinsta\\KMP ) {
+				return;
+			}
+
+			$purge = isset( $kinsta_muplugin->kinsta_cache_purge ) ? $kinsta_muplugin->kinsta_cache_purge : null;
+			if ( ! $purge || ! is_callable( array( $purge, 'initiate_purge' ) ) ) {
+				return;
+			}
+
+			if ( empty( $post_ids ) ) {
+				if ( is_callable( array( $purge, 'purge_complete_caches' ) ) ) {
+					$purge->purge_complete_caches( true );
+				}
+				return;
+			}
+
+			$purged_permalinks = array();
+			foreach ( (array) $post_ids as $post_id ) {
+				$post_id = (int) $post_id;
+				if ( $post_id <= 0 || ! get_post_type( $post_id ) ) {
+					continue;
+				}
+				$purge->initiate_purge( $post_id );
+				$permalink = get_permalink( $post_id );
+				if ( $permalink ) {
+					$purged_permalinks[] = untrailingslashit( preg_replace( '#\\?.*$#', '', $permalink ) );
+				}
+			}
+
+			if ( ! empty( $reffer_url ) && is_string( $reffer_url ) ) {
+				$reffer_url = esc_url_raw( $reffer_url );
+				if ( $reffer_url ) {
+					$ref_normalized = untrailingslashit( preg_replace( '#\\?.*$#', '', $reffer_url ) );
+					if ( ! in_array( $ref_normalized, $purged_permalinks, true ) ) {
+						$base = rtrim( preg_replace( '#\\?.*$#', '', $reffer_url ), '/' );
+						wp_remote_get( $base . '/kinsta-clear-cache/', array( 'sslverify' => false, 'timeout' => 5 ) );
+					}
+				}
+			}
+		}
+
 		/**
 		 * @see https://github.com/bluehost/endurance-page-cache/
 		 */
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/includes/hooks/shortcodes.php /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/includes/hooks/shortcodes.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/includes/hooks/shortcodes.php	2026-02-01 09:28:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/includes/hooks/shortcodes.php	2026-03-05 09:37:42.000000000 +0000
@@ -206,7 +206,7 @@
         }
 
         if( ! empty( $args['template']  ) ){
-            $args['template'] = html_entity_decode( $args['template']  );
+            $args['template'] = wp_ulike_kses( $args['template'] );
         }
 
         $output = sprintf( '<div class="wp_ulike_manual_likers_wrapper wp_%s_likers_%d">%s</div>', esc_attr( $args['type'] ), esc_attr( $args['id'] ),
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/readme.txt	2026-02-09 16:07:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/readme.txt	2026-03-05 09:37:42.000000000 +0000
@@ -6,7 +6,7 @@
 Requires PHP: 7.2.5
 Requires at least: 6.0
 Tested up to: 6.9
-Stable tag: 5.0.1
+Stable tag: 5.0.2
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -179,6 +179,10 @@
 
 == Changelog ==
 
+= 5.0.2 =
+* Added: Kinsta purge cache support.
+* Fixed: Small sanitization improvement.
+
 = 5.0.1 =
 * Improved: Performance optimizations across page loading, statistics display, and data processing for faster response times.
 * Improved: Enhanced security and reliability improvements for widgets and statistics functionality.
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/wp-ulike.php /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/wp-ulike.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.1/wp-ulike.php	2026-02-09 16:07:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-ulike/5.0.2/wp-ulike.php	2026-03-05 09:37:42.000000000 +0000
@@ -3,7 +3,7 @@
  * Plugin Name:       WP ULike
  * Plugin URI:        https://wpulike.com/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash
  * Description:       Looking to increase user engagement on your WordPress site? WP ULike plugin lets you easily add voting buttons to your content. With customizable settings and detailed analytics, you can track user engagement, optimize your content, and build a loyal following.
- * Version:           5.0.1
+ * Version:           5.0.2
  * Author:            TechnoWich
  * Author URI:        https://technowich.com/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash
  * Text Domain:       wp-ulike
@@ -30,7 +30,7 @@
 
 // Do not change these values
 define( 'WP_ULIKE_PLUGIN_URI'   , 'https://wpulike.com/' 		 			);
-define( 'WP_ULIKE_VERSION'      , '5.0.1' 					 		    	);
+define( 'WP_ULIKE_VERSION'      , '5.0.2' 					 		    	);
 define( 'WP_ULIKE_DB_VERSION'   , '2.4' 					 	 			);
 define( 'WP_ULIKE_SLUG'         , 'wp-ulike' 					 			);
 define( 'WP_ULIKE_NAME'         , 'WP ULike'	    						);

Exploit Outline

1. Login to WordPress as a user with at least Contributor-level access. 2. Create a new post or edit an existing one. 3. Add the following shortcode to the post content: `[wp_ulike_likers_box template="&lt;img src=x onerror=alert(document.domain)&gt;"]`. The payload should use HTML entities to bypass the `wp_kses_post` filter during the initial post save. 4. To trigger the XSS, the post must have at least one 'like'. Use the plugin's frontend like button or perform an AJAX request to the `wp_ulike` action with the post ID and valid nonce. 5. View the post. The plugin will execute `html_entity_decode` on the shortcode attribute, rendering the malicious script directly into the page.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.