Published : May 16, 2026

CVE-2026-40799: Simple CAPTCHA Alternative with Cloudflare Turnstile <= 1.38.0 – Broken Authorization (simple-cloudflare-turnstile)

Severity Medium (CVSS 5.3)
CWE 285
Vulnerable Version 1.38.0
Patched Version 1.38.1
Disclosed May 7, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-40799:

This vulnerability allows unauthenticated attackers to bypass Turnstile CAPTCHA verification across multiple forms by reusing a single valid token. The flaw exists in the Simple CAPTCHA Alternative with Cloudflare Turnstile plugin for WordPress (versions up to and including 1.38.0). It is a broken authorization issue (CWE-285) with a CVSS score of 5.3.

Root Cause:
The vulnerable code used PHP sessions to cache successful Turnstile verification across multiple pages or form submissions. In `inc/integrations/ecommerce/woocommerce.php` (lines 113-130), the `cfturnstile_woo_checkout_check` function started a PHP session, then stored a nonce in `$_SESSION[‘cfturnstile_checkout_checked’]` after a successful Turnstile check. On subsequent requests where no `cf-turnstile-response` token was present, the code checked `isset($_SESSION[‘cfturnstile_checkout_checked’])` and used `wp_verify_nonce` to validate the stored nonce, skipping verification entirely. This same pattern appeared in `inc/integrations/ecommerce/edd.php` (lines 22-38), `inc/integrations/membership/memberpress.php` (lines 33-39), `inc/integrations/membership/ultimate-member.php` (lines 17-23), `inc/wordpress.php` (lines 60-73), and `inc/integrations/forms/gravity-forms.php` (lines 54-79). The session-based caching meant that once a user completed any CAPTCHA challenge, they could reuse that verification flag across any form on the same site without presenting a new Turnstile token.

Exploitation:
An attacker can send a single successful Turnstile challenge to the WordPress login page, WooCommerce checkout, EDD checkout, member registration, or any protected form. After the first success, the session stores a verification flag. The attacker can then submit other protected forms (e.g., login, checkout, registration) without including any `cf-turnstile-response` parameter. The server skips the Turnstile API verification because the session flag exists. Requests SHOULD include a valid Turnstile token for the initial session setup; subsequent requests omit the token and reuse the session. The exploit requires the attacker to establish a PHP session (via WordPress cookie handling) and complete one Turnstile challenge. After that, all form submissions within the same session bypass CAPTCHA.

Patch Analysis:
The patch replaces PHP session-based caching with transient-based verification tied to the specific Turnstile token. The new file `inc/verification.php` introduces three functions: `cfturnstile_set_verified`, `cfturnstile_get_verified`, and `cfturnstile_clear_verified`. These store a transient keyed to `md5($key . ‘_t’ . $token)` with a 20-second TTL. Each Turnstile token (which is single-use by design) can only be reused within that same request cycle. The patch removes all `session_start()` calls and `$_SESSION` references. In `woocommerce.php`, the old code checked the session flag and returned early; the new code requires a fresh token for each verification. The `cfturnstile_transient_key` function derives the transient name from the POST’d `cf-turnstile-response` token. This limits re-use to the same token for a very short window (20 seconds). Since Turnstile tokens expire after 300 seconds and can only be verified once by the Cloudflare API, the transient effectively prevents replay across requests.

Impact:
Successful exploitation completely bypasses Turnstile CAPTCHA protection on all forms using the plugin. An attacker can automate brute-force login attacks, spam registration, fraudulent checkout, and other abusive actions without solving CAPTCHA challenges. The plugin is widely used on WordPress sites to reduce spam, so the impact on site security and user trust is significant. Admins would not detect the bypass through normal CAPTCHA failure logs because the verification flag simulates a successful check without actually contacting Cloudflare’s API.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/simple-cloudflare-turnstile/inc/admin/admin-options.php
+++ b/simple-cloudflare-turnstile/inc/admin/admin-options.php
@@ -20,11 +20,14 @@
 add_action('update_option_cfturnstile_key', 'cfturnstile_keys_updated', 10);
 add_action('update_option_cfturnstile_secret', 'cfturnstile_keys_updated', 10);
 function cfturnstile_keys_updated() {
-	update_option('cfturnstile_tested', 'no');
+	update_option( 'cfturnstile_tested', 'no' );
+	delete_option( 'cfturnstile_invalid_secret_notice' );
+	delete_option( 'cfturnstile_soft_tested' );
+	delete_transient( 'cfturnstile_invalid_secret_throttle' );
 }

 // Admin test form to check Turnstile response
-function cfturnstile_admin_test() {
+function cfturnstile_admin_test( $soft = false ) {
 ?>
 	<form action="" method="POST" class="cfturnstile-settings">
 		<?php
@@ -34,19 +37,24 @@
 			$error = '';
 			if (isset($check['success'])) $success = $check['success'];
 			if (isset($check['error_code'])) $error = $check['error_code'];
-			if ($success != true) {
+			if ( $success != true && ! $soft ) {
 				echo '<div style="padding: 20px 20px 25px 20px; margin: 20px 0 28px 0; background: #fff; border-radius: 20px; max-width: 500px; border: 2px solid #d5d5d5;">';
 				echo '<p style="font-weight: 600; font-size: 19px; margin-top: 0; margin-bottom: 0;">' . esc_html__('Almost done...', 'simple-cloudflare-turnstile') . '</p>';
 			}
-			if (!isset($_POST['cf-turnstile-response'])) {
+			if ( ! isset($_POST['cf-turnstile-response']) ) {
+				if(! $soft ) {
 				echo '<p>'
 					. '<span style="color: red; font-weight: bold;">' . esc_html__('API keys have been updated. Please test the Turnstile API response below.', 'simple-cloudflare-turnstile') . '</span>'
 					. '<br/>'
 					. esc_html__('Turnstile will not be added to any forms until the test is successfully complete.', 'simple-cloudflare-turnstile')
 					. '</p>';
+				}
 			} else {
 				if ($success == true) {
-					update_option('cfturnstile_tested', 'yes');
+					update_option( 'cfturnstile_tested', 'yes' );
+					delete_option( 'cfturnstile_invalid_secret_notice' );
+					delete_option( 'cfturnstile_soft_tested' );
+					delete_transient( 'cfturnstile_invalid_secret_throttle' );
 				} else {
 					if ($error == "missing-input-response") {
 						echo '<p style="font-weight: bold; color: red;">' . cfturnstile_failed_message() . '</p>';
@@ -65,7 +73,9 @@
 				echo '<button type="submit" style="margin-top: 10px; padding: 7px 10px; background: #1c781c; color: #fff; font-weight: bold; border: 1px solid #176017; border-radius: 4px; cursor: pointer;">
 				' . esc_html__('TEST RESPONSE', 'simple-cloudflare-turnstile') . ' <span class="dashicons dashicons-arrow-right-alt"></span>
 				</button>';
-				echo '</div>';
+				if ( ! $soft ) {
+					echo '</div>';
+				}
 			}
 		}
 		?>
@@ -75,6 +85,7 @@

 // Show Settings Page
 function cfturnstile_settings_page() {
+
 ?>
 	<div class="sct-wrap wrap">

@@ -124,8 +135,21 @@
 		</div>

 		<?php
-		if (empty(get_option('cfturnstile_tested')) || get_option('cfturnstile_tested') != 'yes') {
+		if ( empty( get_option( 'cfturnstile_tested' ) ) || get_option( 'cfturnstile_tested' ) != 'yes' ) {
 			echo cfturnstile_admin_test();
+		} elseif ( 'no' === get_option( 'cfturnstile_soft_tested' ) ) {
+			// Buffer the test form output so we can process the POST before deciding whether to show the wrapper.
+			ob_start();
+			cfturnstile_admin_test( true );
+			$soft_test_output = ob_get_clean();
+			// Re-check after processing — if the test passed, the flag will have been deleted.
+			if ( 'no' === get_option( 'cfturnstile_soft_tested' ) ) {
+				echo '<div style="padding: 20px 20px 25px 20px; margin: 20px 0 28px 0; background: #fff; border-radius: 20px; max-width: 500px; border: 2px solid #f0c33c;">';
+				echo '<p style="font-weight: 600; font-size: 19px; margin-top: 0; margin-bottom: 0;"><span class="dashicons dashicons-warning" style="color: #f0c33c; font-size: 28px; margin-right: 5px;"></span> ' . esc_html__( 'Re-test Recommended', 'simple-cloudflare-turnstile' ) . '</p>';
+				echo '<p>' . esc_html__( 'Cloudflare reported an invalid secret key error. Turnstile is still active on your forms, but verifications may be failing. Please re-test your API keys below.', 'simple-cloudflare-turnstile' ) . '</p>';
+				echo $soft_test_output;
+				echo '</div>';
+			}
 		}
 		?>

@@ -151,7 +175,7 @@

 						<?php
 						if ( !$cf_const_site && !$cf_const_secret ) {
-							if (get_option('cfturnstile_tested') == 'yes') {
+							if ( get_option('cfturnstile_tested') == 'yes' && 'no' !== get_option( 'cfturnstile_soft_tested' ) ) {
 								echo '<p style=" font-weight: bold; color: #1e8c1e;"><span class="dashicons dashicons-yes-alt"></span> ' . esc_html__('Success! Turnstile is working correctly with your API keys.', 'simple-cloudflare-turnstile') . '</p>';
 							}
 						}
--- a/simple-cloudflare-turnstile/inc/errors.php
+++ b/simple-cloudflare-turnstile/inc/errors.php
@@ -27,6 +27,65 @@
 }

 /**
+ * Display persistent admin warning if an invalid secret key was detected.
+ * Dismissible via AJAX — stays until the admin clicks to dismiss.
+ */
+add_action( 'admin_notices', 'cfturnstile_invalid_secret_notice' );
+function cfturnstile_invalid_secret_notice() {
+	if ( '1' !== get_option( 'cfturnstile_invalid_secret_notice' ) ) {
+		return;
+	}
+	$settings_url = admin_url( 'options-general.php?page=cfturnstile' );
+	$ajax_url     = esc_url( admin_url( 'admin-ajax.php' ) );
+	$nonce        = wp_create_nonce( 'cfturnstile_dismiss_invalid_secret' );
+	?>
+	<div class="notice notice-warning" id="cfturnstile-invalid-secret-notice">
+		<p>
+			<strong><?php esc_html_e( 'Cloudflare Turnstile:', 'simple-cloudflare-turnstile' ); ?></strong>
+			<?php
+			echo wp_kses_post(
+				sprintf(
+					/* translators: %s: URL to the plugin settings page. */
+					__( 'Your Turnstile secret key was rejected by Cloudflare (<code>invalid-input-secret</code>). Please verify your API keys on the <a href="%s">settings page</a>. Turnstile will continue to protect your forms, but verifications may fail until the key is corrected.', 'simple-cloudflare-turnstile' ),
+					esc_url( $settings_url )
+				)
+			);
+			?>
+		</p>
+		<p>
+			<a href="#" id="cfturnstile-dismiss-invalid-secret" class="button button-small">
+				<?php esc_html_e( 'Dismiss', 'simple-cloudflare-turnstile' ); ?>
+			</a>
+		</p>
+	</div>
+	<script>
+	document.getElementById( 'cfturnstile-dismiss-invalid-secret' ).addEventListener( 'click', function( e ) {
+		e.preventDefault();
+		var notice = document.getElementById( 'cfturnstile-invalid-secret-notice' );
+		notice.style.display = 'none';
+		var xhr = new XMLHttpRequest();
+		xhr.open( 'POST', '<?php echo esc_url( $ajax_url ); ?>', true );
+		xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
+		xhr.send( 'action=cfturnstile_dismiss_invalid_secret&_wpnonce=<?php echo esc_attr( $nonce ); ?>' );
+	});
+	</script>
+	<?php
+}
+
+/**
+ * AJAX handler to dismiss the invalid secret notice.
+ */
+add_action( 'wp_ajax_cfturnstile_dismiss_invalid_secret', 'cfturnstile_dismiss_invalid_secret_handler' );
+function cfturnstile_dismiss_invalid_secret_handler() {
+	check_ajax_referer( 'cfturnstile_dismiss_invalid_secret', '_wpnonce' );
+	if ( ! current_user_can( 'manage_options' ) ) {
+		wp_send_json_error( 'Unauthorized', 403 );
+	}
+	delete_option( 'cfturnstile_invalid_secret_notice' );
+	wp_send_json_success();
+}
+
+/**
  * Gets the custom Turnstile failed message
  */
 function cfturnstile_failed_message($default = "") {
--- a/simple-cloudflare-turnstile/inc/integrations/ecommerce/edd.php
+++ b/simple-cloudflare-turnstile/inc/integrations/ecommerce/edd.php
@@ -22,10 +22,9 @@
 	add_action('edd_purchase_form_before_submit', 'cfturnstile_field_edd_checkout', 10);
 	add_action('edd_pre_process_purchase', 'cfturnstile_edd_checkout_check');
 	function cfturnstile_edd_checkout_check() {
-		if (!session_id()) { session_start(); }
-		// Check if already validated
-		if(isset($_SESSION['cfturnstile_edd_checkout_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_edd_checkout_checked']), 'cfturnstile_edd_checkout' )) {
-			unset($_SESSION['cfturnstile_edd_checkout_checked']);
+		// Check if already validated (cache-friendly, no PHP session)
+		if( cfturnstile_get_verified( 'cfturnstile_edd_checkout_checked' ) ) {
+			cfturnstile_clear_verified( 'cfturnstile_edd_checkout_checked' );
 			return;
 		}
 		// Get guest only
@@ -38,8 +37,7 @@
 				if($success != true) {
 					edd_set_error( 'cfturnstile_error', cfturnstile_failed_message() );
 				} else {
-					$nonce = wp_create_nonce( 'cfturnstile_edd_checkout' );
-					$_SESSION['cfturnstile_edd_checkout_checked'] = $nonce;
+					cfturnstile_set_verified( 'cfturnstile_edd_checkout_checked' );
 				}
 			}
 		}
--- a/simple-cloudflare-turnstile/inc/integrations/ecommerce/woocommerce.php
+++ b/simple-cloudflare-turnstile/inc/integrations/ecommerce/woocommerce.php
@@ -113,30 +113,28 @@
 			}
 		}

-		// Start session
-		if (!session_id()) { session_start(); }
-		// Check if already validated
-		if(isset($_SESSION['cfturnstile_checkout_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_checkout_checked']), 'cfturnstile_checkout_check' )) {
-			return;
-		}
-
 		// Check if guest only enabled
 		$guest = esc_attr( get_option('cfturnstile_guest_only') );
-		// Check
+		// Check — always require a fresh Turnstile token (tokens are single-use).
 		if( !$skip && (!$guest || ( $guest && !is_user_logged_in() )) ) {
 			$check = cfturnstile_check();
 			$success = $check['success'];
 			if($success != true) {
 				wc_add_notice( cfturnstile_failed_message(), 'error');
-			} else {
-				$nonce = wp_create_nonce( 'cfturnstile_checkout_check' );
-				$_SESSION['cfturnstile_checkout_checked'] = $nonce;
-				$cfturnstile_wc_checkout_ran = true; // Mark as executed
 			}
+			// Always mark as executed so the second hook doesn't re-verify
+			// the same (now consumed) token and produce duplicate errors.
+			$cfturnstile_wc_checkout_ran = true;
 		}
 	}
 	add_action('woocommerce_store_api_checkout_update_order_from_request', 'cfturnstile_woo_checkout_block_check', 10, 2);
 	function cfturnstile_woo_checkout_block_check($order, $request) {
+		// Prevent duplicate execution within a single request.
+		static $cfturnstile_wc_block_checkout_ran = false;
+		if ( $cfturnstile_wc_block_checkout_ran ) {
+			return;
+		}
+
 		// Skip if Turnstile disabled for payment method
 		$skip = 0;
 		if ( $request->get_method() === 'POST' ) {
@@ -177,31 +175,25 @@
 				}
 			}

-			// Start session
-			if (!session_id()) { session_start(); }
-			// Check if already validated
-			if(isset($_SESSION['cfturnstile_checkout_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_checkout_checked']), 'cfturnstile_checkout_check' )) {
-				return;
-			}
-
 			// Check if guest only enabled
 			$guest = esc_attr( get_option('cfturnstile_guest_only') );
-			// Check
+			// Check — always require a fresh Turnstile token (tokens are single-use).
 			if( !$skip && (!$guest || ( $guest && !is_user_logged_in() )) ) {
 				$extensions = $request->get_param( 'extensions' );
 				$token = ( is_array( $extensions ) && isset( $extensions['simple-cloudflare-turnstile']['token'] ) ) ? $extensions['simple-cloudflare-turnstile']['token'] : '';

 				if ( empty( $token ) ) {
+					$cfturnstile_wc_block_checkout_ran = true;
 					throw new Exception( cfturnstile_failed_message() );
 				}

 				$check = cfturnstile_check( $token );
 				$success = $check['success'];
+				// Always mark as executed so duplicate hooks don't re-verify
+				// the same (now consumed) token and produce duplicate errors.
+				$cfturnstile_wc_block_checkout_ran = true;
 				if($success != true) {
 					throw new Exception( cfturnstile_failed_message() );
-				} else {
-					$nonce = wp_create_nonce( 'cfturnstile_checkout_check' );
-					$_SESSION['cfturnstile_checkout_checked'] = $nonce;
 				}
 			}
 		}
@@ -231,21 +223,7 @@
 		);
 	}
 }
-// On payment complete clear session
-add_action('woocommerce_checkout_order_processed', 'cfturnstile_woo_checkout_clear', 10, 1);
-add_action('woocommerce_store_api_checkout_order_processed', 'cfturnstile_woo_checkout_clear', 10, 1);
-add_action('woocommerce_thankyou', 'cfturnstile_woo_checkout_clear', 10, 1);
-function cfturnstile_woo_checkout_clear($order_id) {
-	if(isset($_SESSION['cfturnstile_checkout_checked'])) { unset($_SESSION['cfturnstile_checkout_checked']); }
-}

-// Additional clears to prevent lingering validation across session changes
-function cfturnstile_woo_clear_session() {
-	if (!session_id()) { session_start(); }
-	if (isset($_SESSION['cfturnstile_checkout_checked'])) { unset($_SESSION['cfturnstile_checkout_checked']); }
-}
-// Logout
-add_action('wp_logout', 'cfturnstile_woo_clear_session', 10, 0);

 // Woo Checkout Pay Order Check
 if(get_option('cfturnstile_woo_checkout_pay')) {
@@ -274,11 +252,8 @@
 			if(defined( 'REST_REQUEST' ) && REST_REQUEST) { return $user; } // Skip REST API
 			if(is_wp_error($user) && isset($user->errors['empty_username']) && isset($user->errors['empty_password']) ) {return $user; } // Skip Errors

-			// Start session
-			if (!session_id()) { session_start(); }
-
-			// Check if already validated
-			if(isset($_SESSION['cfturnstile_login_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_login_checked']), 'cfturnstile_login_check' )) {
+			// Check if already validated (cache-friendly, no PHP session)
+			if( cfturnstile_get_verified( 'cfturnstile_login_checked' ) ) {
 				return $user;
 			}

@@ -288,17 +263,16 @@
 			if($success != true) {
 				$user = new WP_Error( 'cfturnstile_error', cfturnstile_failed_message() );
 			} else {
-				$nonce = wp_create_nonce( 'cfturnstile_login_check' );
-				$_SESSION['cfturnstile_login_checked'] = $nonce;
+				cfturnstile_set_verified( 'cfturnstile_login_checked' );
 			}

 			return $user;

 		}
-		// Clear session on login
+		// Clear verification flag on login
 		add_action('wp_login', 'cfturnstile_woo_login_clear', 10, 2);
 		function cfturnstile_woo_login_clear($user_login, $user) {
-			if(isset($_SESSION['cfturnstile_login_checked'])) { unset($_SESSION['cfturnstile_login_checked']); }
+			cfturnstile_clear_verified( 'cfturnstile_login_checked' );
 		}
 	}
 }
--- a/simple-cloudflare-turnstile/inc/integrations/forms/gravity-forms.php
+++ b/simple-cloudflare-turnstile/inc/integrations/forms/gravity-forms.php
@@ -46,6 +46,7 @@

   function cfturnstile_gravity_check($validation_result)
   {
+    global $cfturnstile_gravity_error;
     $form = $validation_result['form'];
     // if whitelisted or form is disabled, return
     if (cfturnstile_whitelisted() || cfturnstile_form_disable($form['id'], 'cfturnstile_gravity_disable')) {
@@ -54,7 +55,7 @@

     // If not a POST request return
     if ('POST' !== $_SERVER['REQUEST_METHOD']) {
-      $_SESSION['cf-turnstile-response'] = cfturnstile_failed_message();
+      $cfturnstile_gravity_error = cfturnstile_failed_message();
       $validation_result['is_valid'] = false;
       add_filter('gform_validation_message_' . $form['id'], 'cfturnstile_gravity_validation_message', 10, 2);
       return $validation_result;
@@ -64,7 +65,7 @@
     $success = $check['success'];
     // if check fails, return error
     if ($success != true) {
-      $_SESSION['cf-turnstile-response'] = cfturnstile_failed_message();
+      $cfturnstile_gravity_error = cfturnstile_failed_message();
       $validation_result['is_valid'] = false;
       add_filter('gform_validation_message_' . $form['id'], 'cfturnstile_gravity_validation_message', 10, 2);

@@ -76,9 +77,10 @@

   function cfturnstile_gravity_validation_message($message, $form)
   {
-    if (isset($_SESSION['cf-turnstile-response'])) {
-      $error = $_SESSION['cf-turnstile-response'];
-      unset($_SESSION['cf-turnstile-response']);
+    global $cfturnstile_gravity_error;
+    if (isset($cfturnstile_gravity_error)) {
+      $error = $cfturnstile_gravity_error;
+      $cfturnstile_gravity_error = null;

       $message = '<div class="gform_validation_errors" id="gform_' . $form['id'] . '_validation_container">
       <h2 class="gform_submission_error hide_summary"><span class="gform-icon gform-icon--close"></span>
--- a/simple-cloudflare-turnstile/inc/integrations/membership/memberpress.php
+++ b/simple-cloudflare-turnstile/inc/integrations/membership/memberpress.php
@@ -33,12 +33,9 @@
   $LimitedToProductIDs = get_option('cfturnstile_mepr_product_ids');
   $ProductsNeedingCaptcha = explode("n", str_replace("r", "", $LimitedToProductIDs));

-  // Start session
-  if (!session_id()) { session_start(); }
-
-  // Check if already validated
-  if(isset($_SESSION['cfturnstile_login_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_login_checked']), 'cfturnstile_login_check' )) {
-    unset($_SESSION['cfturnstile_login_checked']);
+  // Check if already validated (cache-friendly, no PHP session)
+  if( cfturnstile_get_verified( 'cfturnstile_login_checked' ) ) {
+    cfturnstile_clear_verified( 'cfturnstile_login_checked' );
     return $errors;
   }

@@ -59,8 +56,7 @@
     if($success != true) {
         $errors[] = cfturnstile_failed_message();
     } else {
-      $nonce = wp_create_nonce( 'cfturnstile_login_check' );
-      $_SESSION['cfturnstile_login_checked'] = $nonce;
+      cfturnstile_set_verified( 'cfturnstile_login_checked' );
     }
   } else {
     $errors[] = cfturnstile_failed_message();
--- a/simple-cloudflare-turnstile/inc/integrations/membership/ultimate-member.php
+++ b/simple-cloudflare-turnstile/inc/integrations/membership/ultimate-member.php
@@ -17,9 +17,9 @@
 if(get_option('cfturnstile_um_password')) { add_action( 'um_reset_password_errors_hook', 'cfturnstile_um_check', 20, 1 ); }
 function cfturnstile_um_check( $args ) {

-  // Check if already validated
-  if(isset($_SESSION['cfturnstile_login_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_login_checked']), 'cfturnstile_login_check' )) {
-    unset($_SESSION['cfturnstile_login_checked']);
+  // Check if already validated (cache-friendly, no PHP session)
+  if( cfturnstile_get_verified( 'cfturnstile_login_checked' ) ) {
+    cfturnstile_clear_verified( 'cfturnstile_login_checked' );
     return;
   }

@@ -35,8 +35,7 @@
     if($success != true) {
       UM()->form()->add_error( 'cfturnstile', cfturnstile_failed_message() );
     } else {
-      $nonce = wp_create_nonce( 'cfturnstile_login_check' );
-      $_SESSION['cfturnstile_login_checked'] = $nonce;
+      cfturnstile_set_verified( 'cfturnstile_login_checked' );
   }
   } else {
     UM()->form()->add_error( 'cfturnstile', cfturnstile_failed_message() );
@@ -48,8 +47,8 @@
 function cfturnstile_um_error_message() {
   echo '<p style="color: red; font-weight: bold;">' . cfturnstile_failed_message() . '</p>';
 }
-// Clear session on login
+// Clear verification flag on login
 add_action('um_user_login', 'cfturnstile_um_login_clear', 10, 1);
 function cfturnstile_um_login_clear($args) {
-	if(isset($_SESSION['cfturnstile_login_checked'])) { unset($_SESSION['cfturnstile_login_checked']); }
+	cfturnstile_clear_verified( 'cfturnstile_login_checked' );
 }
 No newline at end of file
--- a/simple-cloudflare-turnstile/inc/turnstile.php
+++ b/simple-cloudflare-turnstile/inc/turnstile.php
@@ -248,12 +248,35 @@
 			$results['success'] = false;
 		}

-		foreach ($response as $key => $val) {
-			if ($key == 'error-codes') {
-				foreach ($val as $key => $error_val) {
+		foreach ( $response as $key => $val ) {
+			if ( 'error-codes' === $key ) {
+				foreach ( $val as $key => $error_val ) {
 					$results['error_code'] = $error_val;
-					if($error_val == 'invalid-input-secret') {
-						update_option('cfturnstile_tested', 'no'); // Disable if invalid secret
+					if ( 'invalid-input-secret' === $error_val ) {
+						// Rate-limit: only process once per 5 minutes to avoid repeated DB writes on high-traffic sites.
+						if ( false === get_transient( 'cfturnstile_invalid_secret_throttle' ) ) {
+							set_transient( 'cfturnstile_invalid_secret_throttle', 1, 5 * MINUTE_IN_SECONDS );
+							$already_flagged = ( 'no' === get_option( 'cfturnstile_soft_tested' ) );
+							update_option( 'cfturnstile_invalid_secret_notice', '1' );
+							update_option( 'cfturnstile_soft_tested', 'no' );
+							if ( ! $already_flagged ) {
+								$admin_email  = get_option( 'admin_email' );
+								$site_name    = get_bloginfo( 'name' );
+								$settings_url = admin_url( 'options-general.php?page=cfturnstile' );
+								$subject      = sprintf(
+									/* translators: %s: Site name. */
+									__( '[%s] Cloudflare Turnstile: Invalid Secret Key Detected', 'simple-cloudflare-turnstile' ),
+									$site_name
+								);
+								$message = sprintf(
+									/* translators: 1: Site name, 2: Settings page URL. */
+									__( "Cloudflare has reported that the Turnstile secret key on %1$s is invalid (error: invalid-input-secret).nnTurnstile is still active on your forms, but verifications may be failing until the key is corrected.nnPlease check your API keys on the settings page:n%2$s", 'simple-cloudflare-turnstile' ),
+									$site_name,
+									$settings_url
+								);
+								wp_mail( $admin_email, $subject, $message );
+							}
+						}
 					}
 				}
 			}
--- a/simple-cloudflare-turnstile/inc/verification.php
+++ b/simple-cloudflare-turnstile/inc/verification.php
@@ -0,0 +1,64 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Build a transient key from the Turnstile token in the current POST.
+ *
+ * @param string $key Verification key, e.g. 'cfturnstile_checkout_checked'.
+ * @return string|false Transient key, or false if no token is present.
+ */
+function cfturnstile_transient_key( $key ) {
+	$token = isset( $_POST['cf-turnstile-response'] ) ? sanitize_text_field( $_POST['cf-turnstile-response'] ) : '';
+	if ( $token ) {
+		return 'cft_' . substr( md5( $key . '_t' . $token ), 0, 20 );
+	}
+
+	return false;
+}
+
+/**
+ * Store a verification flag tied to the current Turnstile token.
+ *
+ * Uses a short-lived transient keyed to the token so each token can only
+ * be used once.  Turnstile tokens are single-use by design.
+ *
+ * @param string $key     Verification key, e.g. 'cfturnstile_checkout_checked'.
+ * @param string $context Reserved for future use (default 'default').
+ */
+function cfturnstile_set_verified( $key, $context = 'default' ) {
+	$transient_key = cfturnstile_transient_key( $key );
+	if ( $transient_key ) {
+		set_transient( $transient_key, 1, 20 );
+	}
+}
+
+/**
+ * Check whether a verification flag is set for the current Turnstile token.
+ *
+ * @param string $key     Verification key, e.g. 'cfturnstile_checkout_checked'.
+ * @param string $context Reserved for future use (default 'default').
+ * @return bool
+ */
+function cfturnstile_get_verified( $key, $context = 'default' ) {
+	$transient_key = cfturnstile_transient_key( $key );
+	if ( $transient_key ) {
+		return (bool) get_transient( $transient_key );
+	}
+
+	return false;
+}
+
+/**
+ * Clear a verification flag.
+ *
+ * @param string $key     Verification key.
+ * @param string $context Reserved for future use (default 'default').
+ */
+function cfturnstile_clear_verified( $key, $context = 'default' ) {
+	$transient_key = cfturnstile_transient_key( $key );
+	if ( $transient_key ) {
+		delete_transient( $transient_key );
+	}
+}
--- a/simple-cloudflare-turnstile/inc/wordpress.php
+++ b/simple-cloudflare-turnstile/inc/wordpress.php
@@ -7,9 +7,7 @@
  * Display the turnstile field on the login form.
  */
 function cfturnstile_field_login() {
-	if(isset($_SESSION['cfturnstile_login_checked'])) {
-		unset($_SESSION['cfturnstile_login_checked']);
-	}
+	cfturnstile_clear_verified( 'cfturnstile_login_checked' );
 	if(get_option('cfturnstile_login_only', 0)) {
 		$login_url_path = wp_parse_url(wp_login_url(), PHP_URL_PATH);
 		$current_url_path = wp_parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
@@ -62,14 +60,11 @@
 			return $user;
 		}

-		// Start session
-		if (!session_id()) { session_start(); }
-
 		// Check if already validated
-		if(isset($user->ID) && isset($_SESSION['cfturnstile_login_checked']) && wp_verify_nonce( sanitize_text_field($_SESSION['cfturnstile_login_checked']), 'cfturnstile_login_check' )) {
+		if(isset($user->ID) && cfturnstile_get_verified( 'cfturnstile_login_checked' ) ) {
 			return $user;
 		} else {
-			unset($_SESSION['cfturnstile_login_checked']);
+			cfturnstile_clear_verified( 'cfturnstile_login_checked' );
 		}

 		// Check Turnstile
@@ -80,18 +75,17 @@
 			do_action('cfturnstile_wp_login_failed');
 		} else {
 			if (isset($user->ID)) {
-				$nonce = wp_create_nonce( 'cfturnstile_login_check' );
-				$_SESSION['cfturnstile_login_checked'] = $nonce;
+				cfturnstile_set_verified( 'cfturnstile_login_checked' );
 			}
 		}

 		return $user;

 	}
-	// Clear session on login
+	// Clear verification flag on login
 	add_action('wp_login', 'cfturnstile_wp_login_clear', 10, 2);
 	function cfturnstile_wp_login_clear($user_login, $user) {
-		if(isset($_SESSION['cfturnstile_login_checked'])) { unset($_SESSION['cfturnstile_login_checked']); }
+		cfturnstile_clear_verified( 'cfturnstile_login_checked' );
 	}
 	/* Hook into wp_login_form() to add the Turnstile field */
 	function cfturnstile_wp_login_form_field($content = "", $args = array()) {
--- a/simple-cloudflare-turnstile/simple-cloudflare-turnstile.php
+++ b/simple-cloudflare-turnstile/simple-cloudflare-turnstile.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: Simple CAPTCHA Alternative with Cloudflare Turnstile
  * Description: Easily add Cloudflare Turnstile to your WordPress forms. The user-friendly, privacy-preserving CAPTCHA alternative.
- * Version: 1.38.0
+ * Version: 1.38.1
  * Author: Elliot Sowersby, RelyWP
  * Author URI: https://www.relywp.com
  * License: GPLv3 or later
@@ -121,6 +121,7 @@
 	 * Include Functions
 	 */
 	include_once(plugin_dir_path(__FILE__) . 'inc/failsafe.php');
+	include_once(plugin_dir_path(__FILE__) . 'inc/verification.php');
 	include_once(plugin_dir_path(__FILE__) . 'inc/turnstile.php');

 	/**
--- a/simple-cloudflare-turnstile/uninstall.php
+++ b/simple-cloudflare-turnstile/uninstall.php
@@ -17,6 +17,12 @@
     }
     // Remove the "cfturnstile_tested" option
     delete_option('cfturnstile_tested');
+    // Remove the "cfturnstile_invalid_secret_notice" option
+    delete_option( 'cfturnstile_invalid_secret_notice' );
+    // Remove the "cfturnstile_soft_tested" option
+    delete_option( 'cfturnstile_soft_tested' );
+    // Remove the throttle transient
+    delete_transient( 'cfturnstile_invalid_secret_throttle' );
     // Remove the "cfturnstile_uninstall_remove" option itself
     delete_option('cfturnstile_uninstall_remove');
 }
 No newline at end of file

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