17 KiB
Security Features Fixes - January 5, 2026
Overview
Fixed critical issues preventing reCAPTCHA v3 and Cloudflare Turnstile from functioning. All security features now properly integrated between settings, frontend, and backend validation.
Issues Found
1. CAPTCHA Scripts Not Loading ❌
Problem: CHECKER_CAPTCHA_HELPER::load_captcha_scripts() was never called in the shortcode rendering.
Impact: reCAPTCHA and Turnstile JavaScript never loaded on pages with checker forms.
2. CAPTCHA Tokens Not Sent to Backend ❌
Problem: AJAX request in public.js didn't include recaptcha_token or turnstile_token fields.
Impact: Backend validation always failed because tokens were missing from the request.
3. reCAPTCHA Token Generation Timing ❌
Problem: reCAPTCHA helper listened to form submit event, but search button uses click event with preventDefault().
Impact: reCAPTCHA tokens never generated because form submission was prevented.
Fixes Implemented
Fix 1: Load CAPTCHA Scripts in Shortcode
File: includes/class-Shortcode.php
Lines: 97-101
// Load CAPTCHA scripts if enabled
if (class_exists('CHECKER_CAPTCHA_HELPER')) {
CHECKER_CAPTCHA_HELPER::load_captcha_scripts($post_id);
}
Result: CAPTCHA scripts now load when shortcode renders.
Fix 2: Send CAPTCHA Tokens in AJAX Request
File: assets/public.js
Lines: 819-837
// Collect CAPTCHA tokens if present
var ajaxData = {
action: "checker_public_validation",
checker_id: $this.data("checker"),
validate: validator,
security: checkerSecurity.nonce,
};
// Add reCAPTCHA token if present
var recaptchaToken = this_checker.find('input[name="recaptcha_token"]').val();
if (recaptchaToken) {
ajaxData.recaptcha_token = recaptchaToken;
}
// Add Turnstile token if present
var turnstileToken = this_checker.find('input[name="turnstile_token"]').val();
if (turnstileToken) {
ajaxData.turnstile_token = turnstileToken;
}
$.ajax({
type: "post",
url: "/wp-admin/admin-ajax.php",
data: ajaxData,
Result: CAPTCHA tokens now sent to backend for validation.
Fix 3: Generate reCAPTCHA Token on Button Click
File: includes/helpers/class-Captcha-Helper.php
Lines: 92-131
function initRecaptchaForForms() {
var forms = document.querySelectorAll(".dw-checker-container form");
forms.forEach(function(form) {
var searchButton = form.querySelector(".search-button");
if (!searchButton) return;
// Generate token when search button is clicked
searchButton.addEventListener("click", function(e) {
var tokenInput = form.querySelector("input[name=recaptcha_token]");
// If token already exists and is recent (less than 2 minutes old), don't regenerate
if (tokenInput && tokenInput.value && tokenInput.dataset.timestamp) {
var tokenAge = Date.now() - parseInt(tokenInput.dataset.timestamp);
if (tokenAge < 120000) { // 2 minutes
return; // Let the click proceed with existing token
}
}
// Generate new token
grecaptcha.ready(function() {
grecaptcha.execute(
window.checkerRecaptcha.siteKey,
{action: window.checkerRecaptcha.action}
).then(function(token) {
// Add or update token in hidden input
if (!tokenInput) {
tokenInput = document.createElement("input");
tokenInput.type = "hidden";
tokenInput.name = "recaptcha_token";
form.appendChild(tokenInput);
}
tokenInput.value = token;
tokenInput.dataset.timestamp = Date.now().toString();
}).catch(function(error) {
console.error("reCAPTCHA error:", error);
});
});
}, true); // Use capture phase to run before other click handlers
});
}
Result: reCAPTCHA tokens now generated on button click with 2-minute caching to avoid excessive API calls.
Security Features Status After Fixes
| Feature | Settings | Frontend | Backend | Status |
|---|---|---|---|---|
| Rate Limiting | ✅ | ✅ | ✅ | WORKING |
| reCAPTCHA v3 | ✅ | ✅ | ✅ | FIXED |
| Cloudflare Turnstile | ✅ | ✅ | ✅ | FIXED |
| URL Parameters | ✅ | ✅ | N/A | WORKING |
API Compatibility Check
Google reCAPTCHA v3
- Endpoint:
https://www.google.com/recaptcha/api/siteverify - Status: ✅ Current (no deprecations)
- Last Updated: 2018 (v3 launch)
- Notes: No breaking changes announced
Cloudflare Turnstile
- Endpoint:
https://challenges.cloudflare.com/turnstile/v0/siteverify - Status: ✅ Current (no deprecations)
- Token Expiry: 300 seconds (5 minutes)
- Notes: Server-side validation is mandatory
Implementation Flow
reCAPTCHA v3 Flow
- User loads page with checker shortcode
load_captcha_scripts()enqueues reCAPTCHA v3 script- Script initializes and attaches click listener to search button
- User clicks search button
- reCAPTCHA generates token (cached for 2 minutes)
- Token stored in hidden input field
- AJAX collects token and sends to backend
- Backend validates token with Google API
- Search proceeds if validation passes
Turnstile Flow
- User loads page with checker shortcode
load_captcha_scripts()enqueues Turnstile script- Script renders visible widget in form
- User completes Turnstile challenge
- Token generated via callback and stored in hidden input
- User clicks search button
- AJAX collects token and sends to backend
- Backend validates token with Cloudflare API
- Search proceeds if validation passes
Testing Checklist
- Enable reCAPTCHA v3 in checker settings
- Verify reCAPTCHA script loads on frontend
- Verify token generated on search button click
- Verify token sent in AJAX request
- Verify backend validation works
- Test with invalid site key (should fail gracefully)
- Enable Turnstile in checker settings
- Verify Turnstile widget renders
- Verify token generated after challenge
- Verify token sent in AJAX request
- Verify backend validation works
- Test rate limiting still works
- Test URL parameters still work
- Verify both CAPTCHAs cannot be enabled simultaneously (UI prevents)
Notes
-
Token Caching: reCAPTCHA tokens are cached for 2 minutes to avoid excessive API calls when users click search multiple times quickly.
-
Event Capture Phase: reCAPTCHA listener uses capture phase (
trueas third parameter) to ensure it runs before the main click handler. -
Turnstile Widget: Turnstile renders a visible widget that users must interact with, unlike reCAPTCHA v3 which is invisible.
-
Backward Compatibility: All changes are backward compatible. Checkers without CAPTCHA enabled continue to work normally.
-
Error Handling: Both CAPTCHA implementations include error callbacks that log to console without breaking the form.
Files Modified
includes/class-Shortcode.php- Added CAPTCHA script loading, require tokens when enabled, added verification to both AJAX handlersassets/public.js- Added token collection and sending to both search button and loadAllData AJAXincludes/helpers/class-Captcha-Helper.php- Fixed reCAPTCHA token generation timing
Additional Fixes (Comprehensive Review - Round 2)
Fix 4: Backend REQUIRES Token When CAPTCHA Enabled
Problem: Backend only verified token if present, but didn't require it when CAPTCHA was enabled.
Impact: Attackers could bypass CAPTCHA by simply not sending a token.
Fix: checker_public_validation() now checks if CAPTCHA is enabled first, then requires token.
Fix 5: Add CAPTCHA Verification to checker_load_all_data
Problem: "Show all" mode only checked rate limiting, not CAPTCHA.
Impact: "Show all" mode bypassed CAPTCHA protection entirely.
Fix: Added same CAPTCHA verification logic to checker_load_all_data().
Fix 6: Add CAPTCHA Tokens to loadAllData AJAX
Problem: Frontend loadAllData() function didn't send CAPTCHA tokens.
Impact: Even if backend required tokens, they weren't sent.
Fix: Added token collection and sending to loadAllData() AJAX request.
Known Limitation
Show All Mode + CAPTCHA Timing
When "show all" mode is enabled with CAPTCHA:
- reCAPTCHA v3: Tokens are generated on search button click, not on page load
- Turnstile: Widget must render and user must complete challenge first
Current behavior: Initial page load in "show all" mode will fail CAPTCHA verification if CAPTCHA is enabled.
Recommendation: Either:
- Disable CAPTCHA requirement for "show all" mode (less secure)
- Show form first, require user interaction before loading data (more secure)
- Auto-generate reCAPTCHA token on page load for "show all" mode
Status: ✅ All fixes implemented and ready for testing Date: January 5, 2026 Version: 1.5.0+
Comprehensive Implementation Update (Round 2)
Refactoring Completed
1. Unified Security Verification Method
File: includes/class-Security.php
- Added
CHECKER_SECURITY::is_enabled($checker, $feature)- Check if security feature is enabled - Added
CHECKER_SECURITY::get_setting($checker, $feature, $key, $default)- Get setting with default - Added
CHECKER_SECURITY::get_error_message($checker, $feature, $default_key)- Get custom or default i18n message - Added
CHECKER_SECURITY::verify_all_security($checker_id, $checker, $request, $skip_captcha)- Unified verification - Added
CHECKER_SECURITY::check_nonce_status($nonce, $action)- Enhanced nonce checking with expiry detection
2. Consolidated AJAX Data Building (JavaScript)
File: assets/public.js
- Added
buildSecureAjaxData(checkerId, baseData)- Build AJAX data with all security tokens - Added
handleAjaxError(xhr, checkerId)- Unified error handling - Added
showSecurityError(checkerId, message, requireRefresh)- Display security errors - Added
resetCaptchaWidget(checkerId, captchaType)- Reset CAPTCHA after error - Added
needsCaptchaRefresh(checkerId, captchaType)- Check if token needs refresh - Added
refreshRecaptchaToken(checkerId)- Refresh reCAPTCHA token before expiry - Added
getAjaxUrl()- Get AJAX URL from localized variable
3. Refactored Backend Handlers
File: includes/class-Shortcode.php
checker_public_validation()now usesverify_all_security()checker_load_all_data()now usesverify_all_security()with show-all mode handling- Both handlers now check nonce status with proper expiry handling
Must-Have Features Implemented
1. Show All Mode + CAPTCHA Conflict Handling
- Added
initial_loadparameter to differentiate initial page load - Backend skips CAPTCHA for initial load if configured (still checks rate limit and honeypot)
- Frontend passes
initial_load: "yes"on first load
2. CAPTCHA Token Refresh Before Expiry
- reCAPTCHA tokens refresh at 90 seconds (before 2-minute expiry)
- Turnstile tokens refresh at 4 minutes (before 5-minute expiry)
- Automatic refresh before search submission
3. Error Message Customization
- Added custom error message fields for reCAPTCHA in admin UI
- Added custom error message fields for Turnstile in admin UI
- Backend uses custom message if set, falls back to translatable default
Nice-to-Have Features Implemented
1. Security Dashboard Enhancements
File: admin/class-Security-Dashboard.php
- Added Honeypot tracking to security overview
- Added Honeypot column to individual checker status table
- Updated protection status check to include honeypot
- Shows count of honeypot-enabled checkers
2. Honeypot Field Implementation
Files: Multiple
- Added honeypot settings section in security tab (
setting-table-security.php) - Added honeypot hidden field to shortcode output (
class-Shortcode.php) - Added honeypot verification in unified security check (
class-Security.php) - Added honeypot value collection in AJAX data (
public.js) - Added JavaScript toggle for honeypot settings
3. Multi-language CAPTCHA Error Messages
File: includes/class-Shortcode.php, includes/class-Security.php
- Localized all error messages with
__()function - Added i18n object to
checkerSecuritylocalized script - Error messages include: refresh_page, session_expired, recaptcha_failed, turnstile_failed, rate_limited, security_error, loading, searching, error_occurred
4. Nonce Expiry Handling
Files: includes/class-Security.php, assets/public.js
- Added
check_nonce_status()method with expiry detection - Returns whether nonce is valid, expired, or expiring soon
- Frontend shows "Refresh Page" button when nonce expires
- Proper error type
nonce_expiredfor frontend handling
Complete Feature Flow Verification
Search Button Click Flow
- ✅ User clicks search button
- ✅ Remove any existing security errors
- ✅ Check if reCAPTCHA token needs refresh → refresh if needed
- ✅ Build secure AJAX data with
buildSecureAjaxData() - ✅ Send AJAX to
getAjaxUrl()(not hardcoded) - ✅ Backend checks nonce status
- ✅ Backend calls
verify_all_security():- ✅ Check honeypot (if enabled)
- ✅ Check rate limit (if enabled)
- ✅ Check reCAPTCHA (if enabled) - require token
- ✅ Check Turnstile (if enabled) - require token
- ✅ Return error with type and i18n message if any check fails
- ✅ Frontend handles error via
handleAjaxError() - ✅ Show user-friendly error with refresh button if nonce expired
Show All Mode Flow
- ✅ Page loads with checker
- ✅ CAPTCHA scripts load via
load_captcha_scripts() - ✅
loadAllData()called withisInitialLoad: true - ✅ Build secure AJAX data (tokens may not be ready yet)
- ✅ Backend checks nonce
- ✅ Backend calls
verify_all_security()withskip_captcha = truefor initial load - ✅ Still checks honeypot and rate limit
- ✅ Data loads successfully
- ✅ Subsequent searches require full CAPTCHA verification
Honeypot Flow
- ✅ Admin enables honeypot in security settings
- ✅ Shortcode renders invisible honeypot field
- ✅ Real users never see or fill it
- ✅ Bots auto-fill the field
- ✅ Frontend collects honeypot value in AJAX
- ✅ Backend checks honeypot first (fastest check)
- ✅ If filled → reject with generic error (don't reveal honeypot detection)
Nonce Expiry Flow
- ✅ User opens page → nonce created
- ✅ After 12-24 hours → nonce expires
- ✅ User tries to search
- ✅ Backend detects expired nonce
- ✅ Returns
type: "nonce_expired"with i18n message - ✅ Frontend shows error with "Refresh Page" button
- ✅ User clicks → page reloads with fresh nonce
Files Modified (Complete List)
includes/class-Security.php- Added helper methods, unified verification, nonce status checkincludes/class-Shortcode.php- Refactored AJAX handlers, added honeypot, i18n localizationassets/public.js- Added helper functions, error handling, token refresh, AJAX URL fixincludes/helpers/class-Captcha-Helper.php- Fixed reCAPTCHA token generation timingtemplates/editor/setting-table-security.php- Added error message fields, honeypot settingsadmin/class-Security-Dashboard.php- Added honeypot tracking
Testing Checklist
Security Features
- Rate limiting blocks after max attempts
- Rate limiting respects time window and block duration
- IP whitelist bypasses rate limiting
- reCAPTCHA v3 loads and generates token
- reCAPTCHA token refreshes before expiry
- reCAPTCHA verification works with Google API
- Turnstile widget renders and generates token
- Turnstile verification works with Cloudflare API
- Honeypot field is invisible to users
- Honeypot blocks bots that fill it
- Custom error messages display correctly
- Nonce expiry shows refresh button
- Show-all mode loads without CAPTCHA on initial load
Admin UI
- Security tab displays correctly
- Toggles show/hide settings sections
- Only one CAPTCHA can be active at a time
- Security status updates dynamically
- Security dashboard shows all checker statuses
- Honeypot column appears in dashboard
i18n
- All error messages are translatable
- Frontend receives i18n strings via localization
Final Status: ✅ All implementations complete Date: January 5, 2026 Version: 1.5.1