CVE-2026-2389

Complianz – GDPR/CCPA Cookie Consent <= 7.4.4.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via Content Filter

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

Description

The Complianz – GDPR/CCPA Cookie Consent plugin for WordPress is vulnerable to Stored Cross-Site Scripting in all versions up to, and including, 7.4.4.2. This is due to the `revert_divs_to_summary` function replacing `&#8221;` HTML entities with literal double-quote characters (`"`) in post content without subsequent sanitization. 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 the injected page. The Classic Editor plugin is required to be installed and activated in order to exploit this vulnerability.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=7.4.4.2
PublishedMarch 25, 2026
Last updatedMarch 26, 2026
Affected plugincomplianz-gdpr

What Changed in the Fix

Changes introduced in v7.4.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2389 - Complianz GDPR Stored XSS ## 1. Vulnerability Summary The **Complianz – GDPR/CCPA Cookie Consent** plugin (<= 7.4.4.2) is vulnerable to Stored Cross-Site Scripting (XSS) due to an unsafe string replacement logic in the `revert_divs_to_summary` function …

Show full research plan

Exploitation Research Plan: CVE-2026-2389 - Complianz GDPR Stored XSS

1. Vulnerability Summary

The Complianz – GDPR/CCPA Cookie Consent plugin (<= 7.4.4.2) is vulnerable to Stored Cross-Site Scripting (XSS) due to an unsafe string replacement logic in the revert_divs_to_summary function (typically located in content processing logic). The function replaces the HTML entity &#8221; (Right Double Quotation Mark) with literal double-quote characters (") in the post content.

This replacement occurs during the rendering phase (via a filter like the_content) after WordPress's core security filters (like wp_filter_post_kses) have already processed and sanitized the content. By using the entity &#8221;, an attacker can bypass the initial sanitization that strips or encodes literal quotes in dangerous attributes (e.g., onerror), only to have the plugin "re-arm" the payload into executable HTML when the page is viewed.

2. Attack Vector Analysis

  • Endpoint: wp-admin/post.php (for saving content) or any endpoint that updates post content.
  • Hook/Action: The plugin hooks into the_content (or a similar document generation filter) to process placeholders.
  • Vulnerable Parameter: post_content.
  • Authentication Level: Contributor or higher (any user role allowed to create/edit posts).
  • Preconditions:
    1. The Classic Editor plugin must be installed and activated.
    2. The Complianz plugin must be active.

3. Code Flow

  1. Entry Point: A Contributor user saves a post containing the payload: <img src=x onerror=&#8221;alert(1)&#8221;>.
  2. Storage: WordPress core sanitizes the post. Since &#8221; is a standard HTML entity for a curly quote and not a literal quote, it is often permitted within attribute values during the initial wp_kses pass for Contributors.
  3. Execution (Sink): When a user (e.g., Admin) views the post:
    • WordPress triggers the the_content filter.
    • The plugin's filter (likely registered in class-document.php or functions.php) executes.
    • The function revert_divs_to_summary (inferred) is called on the content.
    • Vulnerable Line: str_replace('&#8221;', '"', $content) (or equivalent logic).
    • The resulting HTML becomes: <img src=x onerror="alert(1)">.
    • The browser executes the JavaScript in the onerror handler.

4. Nonce Acquisition Strategy

This exploit leverages the standard WordPress post-creation flow. To save a post as a Contributor via the http_request tool, a _wpnonce is required.

  1. Identify Flow: Navigate to the "Add New Post" page.
  2. Action: Use browser_navigate to wp-admin/post-new.php?classic-editor.
  3. Extraction: Use browser_eval to extract the _wpnonce from the form.
    • Variable: document.querySelector('#_wpnonce').value
  4. Alternative: If using the REST API to update content, navigate to any admin page and extract the wp_rest nonce from the wpApiSettings object.
    • Variable: window.wpApiSettings?.nonce

5. Exploitation Strategy

Step 1: Environment Setup

  1. Ensure the Classic Editor plugin is active.
  2. Ensure Complianz GDPR is active.
  3. Create a Contributor user.

Step 2: Content Injection

Perform an authenticated POST request as the Contributor to create a post containing the payload.

  • URL: http://localhost:8080/wp-admin/post.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • action: editpost
    • post_ID: [NEW_POST_ID] (obtained from post-new.php or by creating a draft first)
    • _wpnonce: [EXTRACTED_NONCE]
    • post_title: XSS Test
    • post_content: <img src=x onerror=&#8221;alert(document.domain)&#8221;>
    • publish: Publish (or submit_for_review)

Step 3: Trigger Execution

Navigate to the front-end URL of the newly created post (e.g., /?p=[ID]).

6. Test Data Setup

  1. Classic Editor: wp plugin install classic-editor --activate
  2. Contributor User: wp user create attacker attacker@example.com --role=contributor --user_pass=password123
  3. Plugin Config: Default Complianz configuration is sufficient, as the content filter is generally global for document/cookie policy integration.

7. Expected Results

  • Stored State: Querying the database wp_posts table for the injected ID shows the literal string &#8221;.
  • Rendered State: Viewing the post HTML source in the browser shows <img src=x onerror="alert(document.domain)">.
  • Action: An alert box showing the site domain appears in the browser.

8. Verification Steps

  1. Database Check:
    wp db query "SELECT post_content FROM wp_posts WHERE post_title='XSS Test' LIMIT 1"
    # Verify output contains &#8221;
    
  2. HTTP Verification:
    Use http_request to GET the post permalink and check if the literal onerror=" (with straight quote) exists in the response body.
    const response = await http_request.get("http://localhost:8080/?p=[ID]");
    if (response.body.includes('onerror="alert')) {
        console.log("Exploit Verified: Entity converted to literal quote.");
    }
    

9. Alternative Approaches

If the the_content filter is not targeted directly, the vulnerability may exist in the "Legal Document" generation path.

Backup Strategy:

  1. Check if Complianz provides a shortcode like [cmplz-document type="cookie-statement"].
  2. If the XSS payload is injected into a section that the plugin pulls into its generated documents (like a custom footer or data request page), the same revert_divs_to_summary logic may apply during the document rendering.
  3. Attempt to inject the payload via the plugin's "Processing Agreements" or "Data Requests" fields if those are accessible to Contributor+ roles or via an unauthenticated data request form (if improperly sanitized).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Complianz plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) due to an unsafe string replacement in its content filtering logic. The plugin's `revert_divs_to_summary` function replaces the HTML entity `&#8221;` (right curly double quote) with a literal double quote (`"`) during the rendering phase, after standard WordPress sanitization filters have already run. This allow attackers with Contributor-level access to bypass security checks by using entities that are later converted into executable JavaScript attributes (e.g., in event handlers like `onerror`).

Vulnerable Code

// Inferred from research plan; likely located in documents/class-document.php
// The following logic replaces curly quote entities with literal quotes without further sanitization:
str_replace('&#8221;', '"', $content)

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/complianz-gdpr/7.4.4.2/assets/css/admin.css /home/deploy/wp-safety.org/data/plugin-versions/complianz-gdpr/7.4.5/assets/css/admin.css
--- /home/deploy/wp-safety.org/data/plugin-versions/complianz-gdpr/7.4.4.2/assets/css/admin.css	2025-10-30 17:46:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/complianz-gdpr/7.4.5/assets/css/admin.css	2026-03-16 09:11:22.000000000 +0000
@@ -1001,83 +1001,83 @@
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(1) {
-    width: 78%;
+    width: 93%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(2) {
-    width: 64%;
+    width: 83%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(3) {
-    width: 61%;
+    width: 63%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(4) {
-    width: 92%;
+    width: 65%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(5) {
-    width: 67%;
+    width: 62%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(6) {
-    width: 66%;
+    width: 63%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(7) {
-    width: 61%;
+    width: 91%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(8) {
-    width: 66%;
+    width: 65%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(9) {
-    width: 99%;
+    width: 63%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(10) {
-    width: 67%;
+    width: 68%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(11) {
-    width: 97%;
+    width: 77%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(12) {
-    width: 94%;
+    width: 84%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(13) {
-    width: 80%;
+    width: 90%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(14) {
-    width: 83%;
+    width: 66%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(15) {
-    width: 70%;
+    width: 78%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(16) {
-    width: 62%;
+    width: 89%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(17) {
-    width: 80%;
+    width: 69%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(18) {
-    width: 85%;
+    width: 93%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(19) {
-    width: 99%;
+    width: 93%;
 }
 
 .cmplz-placeholder .cmplz-placeholder-line:nth-of-type(20) {
-    width: 100%;
+    width: 66%;
 }

Exploit Outline

The exploit requires the Classic Editor plugin to be active and an authenticated user with at least Contributor-level permissions. An attacker creates or edits a post and injects an HTML payload using the `&#8221;` entity to wrap attribute values that would otherwise be sanitized if using literal double quotes (e.g., `<img src=x onerror=&#8221;alert(document.domain)&#8221;>`). When a user views this post, the Complianz plugin's content filter triggers the `revert_divs_to_summary` function, which replaces the entities with literal quotes. This results in the final rendered HTML having a valid JavaScript event handler: `<img src=x onerror="alert(document.domain)">`, causing the browser to execute the script.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.