WP ULike <= 5.0.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attribute
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:NTechnical Details
What Changed in the Fix
Changes introduced in v5.0.2
Source Code
WordPress.org SVNThis 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)
- Entry Point: A user with Contributor+ permissions saves a post containing:
[wp_ulike_likers_box template="<script>alert(1)</script>"] - Shortcode Registration: The plugin registers the shortcode (likely via
add_shortcode( 'wp_ulike_likers_box', ... )). - Shortcode Execution: When the post is viewed, WordPress calls the plugin's callback function for this shortcode.
- Insecure Processing: The callback function extracts the
templateattribute and applieshtml_entity_decode($atts['template']). - 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.
- Identify Shortcode Page: The
wp_ulikescripts and nonces are typically enqueued on pages where a like button exists. - Create Trigger Post: Use WP-CLI to create a post containing the plugin's default like button and the malicious shortcode.
- Navigate and Extract:
- Navigate to the post URL using
browser_navigate. - Use
browser_evalto extract the localized nonce. - Inferred JS Variable:
window.wp_ulike_params?.nonce(Verify this viabrowser_eval("window.wp_ulike_params")).
- Navigate to the post URL using
5. Exploitation Strategy
This exploit requires two phases: Injection and Triggering.
Phase 1: Injection (Contributor)
- Log in as a Contributor.
- Create a new post with a payload encoded in the
templateattribute.- Payload:
[wp_ulike_likers_box template="<img src=x onerror=alert(document.domain)>"] - Note: We also need a standard like button on the page to facilitate "liking" it.
- Shortcode Body:
[wp_ulike] [wp_ulike_likers_box template="<img src=x onerror=alert(document.domain)>"]
- Payload:
Phase 2: Triggering (Subscriber or Anon)
- Navigate to the newly created post.
- Identify the ID of the post.
- Extract the
wp_ulikenonce from the page source. - Perform an AJAX request to "like" the post.
- Action:
wp_ulike - Endpoint:
/wp-admin/admin-ajax.php - Parameters:
action:wp_ulikeid: [Post ID]nonce: [Extracted Nonce]type:poststatus:1
- Action:
6. Test Data Setup
- Users:
- Create a user
contributor_userwith thecontributorrole.
- Create a user
- Plugin Configuration: Ensure "Like" functionality is enabled for posts (default behavior).
- 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="<img src=x onerror=alert(document.domain)>"]'
7. Expected Results
- When Phase 2 (the Like) is completed, the database table
wp_ulike(or similar) will contain an entry for the post ID. - When any user (including Admin) visits the post, the plugin will render the likers box.
- The
templateattribute will be decoded from<img ... >to<img ... >. - The browser will attempt to load the image with source
x, fail, and execute theonerrorhandler, triggering thealert(document.domain).
8. Verification Steps
- Check Database for Like:
wp db query "SELECT * FROM wp_ulike WHERE item_id = [POST_ID]" - Verify Raw Output:
Usehttp_requestto 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:
<script>alert(1)</script> - Hex Entities:
<script>alert(1)</script> - Different Sink Context: If the
templateattribute is rendered inside a specific container, try breaking out of attributes:template='"><script>alert(1)</script>'. (Since the bug specifically citeshtml_entity_decodebypassingwp_kses_post, the entity approach is most likely).
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 @@ -1,4 +1,4 @@ -/*! WP ULike - v5.0.1 +/*! WP ULike - v5.0.2 * https://wpulike.com * TechnoWich 2026; */ @@ -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/ */ @@ -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'] ), @@ -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. @@ -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="<img src=x onerror=alert(document.domain)>"]`. 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.