CVE-2026-2834

Age Verification & Identity Verification by Token of Trust <= 3.32.3 - Unauthenticated Stored Cross-Site Scripting via 'description' Parameter

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
3.32.4
Patched in
1d
Time to patch

Description

The Age Verification & Identity Verification by Token of Trust plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the ‘description’ parameter in all versions up to, and including, 3.32.3 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.32.3
PublishedApril 14, 2026
Last updatedApril 15, 2026
Affected plugintoken-of-trust

What Changed in the Fix

Changes introduced in v3.32.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Verified by PoC

# Exploitation Research Plan: CVE-2026-2834 (Stored XSS in Token of Trust) ## 1. Vulnerability Summary The **Age Verification & Identity Verification by Token of Trust** plugin is vulnerable to unauthenticated stored cross-site scripting (XSS). The vulnerability exists in the handling of the `tot_e…

Show full research plan

Exploitation Research Plan: CVE-2026-2834 (Stored XSS in Token of Trust)

1. Vulnerability Summary

The Age Verification & Identity Verification by Token of Trust plugin is vulnerable to unauthenticated stored cross-site scripting (XSS). The vulnerability exists in the handling of the tot_error_log AJAX action. An unauthenticated attacker can send a request to admin-ajax.php with a malicious payload in the description parameter. This payload is stored in the WordPress database (within the tot_logs option) and is subsequently rendered in the plugin's administration dashboard without proper sanitization or output escaping.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tot_error_log (registered as unauthenticated/nopriv)
  • Vulnerable Parameter: description
  • Authentication: None required (Unauthenticated)
  • Preconditions: The Debugger class requires "debug mode" to be active to record logs. However, Modules/Verification/Shared/Debugger.php checks for a debug_mode parameter or cookie, which an attacker can easily provide to bypass this check.

3. Code Flow

  1. Entry Point: The frontend JavaScript Modules/Shared/Assets/tot-error-log.js defines the totErrorLog function, which makes a POST request to admin-ajax.php with the action tot_error_log and parameters description, severity, module, and error.
  2. AJAX Handling: The plugin registers wp_ajax_nopriv_tot_error_log. This handler calls the Debugger::log() method found in Modules/Verification/Shared/Debugger.php.
  3. Logic Bypass: In Debugger.php, line 99:
    if ( ! tot_debug_mode() && ! \TOT\Shared\Settings::get_param_or_cookie( 'debug_mode' ) ) {
        return;
    }
    
    An attacker can satisfy this condition by providing a debug_mode cookie or POST parameter.
  4. Storage: The Debugger::log method stores the description (passed as $head) into the $new_logs array. Upon shutdown, Debugger::store_logs_to_db() (line 147) is called:
    $db_logs = get_option( 'tot_logs', array() );
    // ... unshifts new logs ...
    update_option( 'tot_logs', $db_logs, false );
    
  5. Execution Sink: When an administrator visits the plugin settings or logging page (likely handled by Modules/Shared/Settings/Page.php or a dedicated log view), the values in the tot_logs option are retrieved and echoed into the HTML response without using escaping functions like esc_html().

4. Nonce Acquisition Strategy

Based on the analysis of Modules/Shared/Assets/tot-error-log.js, the tot_error_log action does not use a nonce. The $.ajax call specifically omits any nonce or security tokens, which is common for frontend error logging where nonces might expire or not be available to unauthenticated users.

Conclusion: No nonce is required for this exploit.

5. Exploitation Strategy

The exploit will be performed using a single unauthenticated HTTP POST request.

Step 1: Inject Payload

Send a POST request to admin-ajax.php to trigger the logging mechanism.

  • URL: http://[target]/wp-admin/admin-ajax.php
  • Method: POST
  • Headers:
    • Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • action: tot_error_log
    • description: <script>alert("XSS_EXPLOITED")</script>
    • severity: error
    • module: frontend-logger
    • debug_mode: 1 (This bypasses the tot_debug_mode() check)
    • error: {}

Step 2: Trigger Execution

Log in as an administrator and navigate to the plugin's main settings page where logs are displayed.

  • URL: http://[target]/wp-admin/admin.php?page=tot_settings_tot_settings (The slug is derived from Page.php line 52: tot_settings_ + slugified title).

6. Test Data Setup

  1. Install and activate Age Verification & Identity Verification by Token of Trust version 3.32.3.
  2. Ensure the plugin is initialized (usually occurs upon visiting the settings page once as admin).
  3. No special content or shortcodes are required because the endpoint is an AJAX action available globally via admin-ajax.php.

7. Expected Results

  1. The AJAX request should return a successful JSON response: {"success": true} or similar, and the console.log in tot-error-log.js would indicate success.
  2. The database option tot_logs will now contain the malicious <script> tag.
  3. Upon navigating to the plugin's settings/logs page as an admin, a JavaScript alert box with "XSS_EXPLOITED" will appear.

8. Verification Steps

After sending the HTTP request, verify the injection using WP-CLI:

# Check if the payload is present in the tot_logs option
wp option get tot_logs --format=json | grep "XSS_EXPLOITED"

To verify the "debug mode" bypass:

# Check if logs were written even if global debug mode is off
wp option get tot_options | grep "debug_mode" # Should be empty or 0 if default

9. Alternative Approaches

If the description parameter is sanitized, try injecting through the error parameter.

  • Payload 2: Inject into the error parameter. Debugger.php line 125 uses print_r($log, true) for the body. If the error data is displayed alongside the description, XSS may trigger there.
  • Bypass Check: If debug_mode as a POST parameter fails, try setting it as a cookie:
    # Using browser_navigate or http_request with cookies
    Cookie: debug_mode=1
    
  • Payload 3: If <script> tags are blocked, use attribute-based XSS:
    <img src=x onerror=alert(1)>
    
Research Findings

Summary

The Token of Trust plugin for WordPress (<= 3.32.3) allows unauthenticated attackers to perform stored cross-site scripting (XSS) via the 'tot_error_log' AJAX action. Malicious JavaScript injected into the 'description' parameter is stored in the database and executed when an administrator views the plugin's settings or error logs.

Vulnerable Code

// admin/error-log.php (approx lines 7-14 in 3.32.3)
tot_add_action( 'wp_ajax_nopriv_tot_error_log', 'tot_handle_error_log' );

function tot_handle_error_log() {
	$max_post_size = 64000; // 64KB

	if ( ! isset( $_POST['description'] ) || empty( $_POST['description'] ) ) {
		wp_send_json_success( array( 'message' => 'Empty error description, not logged.' ) );
		wp_die();
	}

	$description = trim( $_POST['description'] );

---

// Modules/Verification/Shared/Debugger.php (approx lines 99-102 in 3.32.3)
		// Don't store if the debug mode is not active
		if ( ! tot_debug_mode() && ! \TOT\Shared\Settings::get_param_or_cookie( 'debug_mode' ) ) {
			return;
		}

---

// Modules/Verification/Shared/Debugger.php (approx lines 130-143 in 3.32.3)
		$new_log = array(
			'timestamp' => current_time( 'mysql' ),
			'body'      => print_r( $log, true ),
			'type'      => $type,
		);

		if ( ! empty( $head ) ) {
			$new_log['head']   = $head;
			$this->new_heads[] = $head;
		}

		$this->new_logs[] = $new_log;

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.3/admin/enqueue-js.php /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.4/admin/enqueue-js.php
--- /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.3/admin/enqueue-js.php	2026-03-16 23:45:22.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.4/admin/enqueue-js.php	2026-03-25 12:47:52.000000000 +0000
@@ -42,6 +42,7 @@
 			'appDomain'                    => tot_get_setting_prod_domain() ?: parse_url( home_url( '/' ) )['host'],
 			'restUrl'                      => esc_url_raw( rest_url() ),
 			'nonce'                        => wp_create_nonce( 'tot_rest' ),
+			'errorLogNonce'                => wp_create_nonce( 'tot-error-log' ),
 			'appUserEmail'                 => $app_user_email,
 			'verificationRequiredPageLink' => $verification_required_edit_post_link,
 		)
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.3/admin/error-log.php /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.4/admin/error-log.php
--- /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.3/admin/error-log.php	2026-03-16 23:45:22.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/token-of-trust/3.32.4/admin/error-log.php	2026-03-25 12:47:52.000000000 +0000
@@ -5,33 +5,32 @@
 tot_add_action( 'wp_ajax_nopriv_tot_error_log', 'tot_handle_error_log' );
 
 function tot_handle_error_log() {
-	$max_post_size = 64000; // 64KB
+	$max_post_size = 64000;
 
-	if ( ! isset( $_POST['description'] ) || empty( $_POST['description'] ) ) {
-		wp_send_json_success( array( 'message' => 'Empty error description, not logged.' ) );
+	if ( ! wp_verify_nonce( $_POST['_ajax_nonce'] ?? '', 'tot-error-log' ) ) {
+		wp_send_json_error( 'Invalid security token.' );
 		wp_die();
 	}
 
-	$description = trim( $_POST['description'] );
+	$description = sanitize_textarea_field( trim( $_POST['description'] ?? '' ) );
 
-	// Double-check after trimming
 	if ( empty( $description ) ) {
 		wp_send_json_success( array( 'message' => 'Empty error description, not logged.' ) );
 		wp_die();
 	}
 
-	$module   = $_POST['module'] ?? null;
-	$severity = $_POST['severity'] ?? 'error';
+	$module   = sanitize_text_field( $_POST['module'] ?? '' ) ?: null;
+	$severity = tot_sanitize_severity( $_POST['severity'] ?? 'error' );
 
 	$error_data = array();
 
-	( isset( $_POST['error'] ) ) {
+	if ( ! empty( $_POST['error'] ) ) {
 		$posted_error = json_decode( stripslashes( $_POST['error'] ), true );
 
 		if ( is_array( $posted_error ) ) {
-			$error_data = $posted_error;
-		} elseif ( ! empty( $_POST['error'] ) ) {
-			$error_data['error'] = $posted_error;
+			$error_data = tot_sanitize_error_data( $posted_error );
+		} else {
+			$error_data['error'] = wp_strip_all_tags( (string) $posted_error );
 		}
 	}
 
@@ -39,12 +38,10 @@
 		$error_data['module'] = $module;
 	}
 
-	// Serialize and truncate if necessary
-	$serialized = print_r( $error_data, true );
+	$serialized = wp_strip_all_tags( print_r( $error_data, true ) );
+
 	if ( strlen( $serialized ) > $max_post_size ) {
-		$truncated  = substr( $serialized, 0, $max_post_size - 1000 );
-		$truncated .= "\n\n[Truncated: data exceeded 64KB limit]";
-		$serialized = $truncated;
+		$serialized = substr( $serialized, 0, $max_post_size - 1000 ) . "\n\n[Truncated: data exceeded 64KB limit]";
 	}
 
 	Debugger::inst()->log( $description, $serialized, $severity );

Exploit Outline

The exploit is executed via an unauthenticated AJAX request. 1. An attacker sends a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `tot_error_log`. 2. The `description` parameter is populated with a malicious script payload (e.g., `<script>alert("XSS")</script>`). 3. To satisfy the plugin's debug check, the attacker includes a `debug_mode` parameter or cookie set to `1`. 4. The plugin stores this payload in the `tot_logs` option in the WordPress database. 5. The XSS executes when an administrator visits the plugin's settings page at `admin.php?page=tot_settings_tot_settings` (or similar logging views), as the stored description is rendered without proper output escaping.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.