Files
dw-sheet-data-checker/SECURITY_FIXES_2026-01-05.md

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

  1. User loads page with checker shortcode
  2. load_captcha_scripts() enqueues reCAPTCHA v3 script
  3. Script initializes and attaches click listener to search button
  4. User clicks search button
  5. reCAPTCHA generates token (cached for 2 minutes)
  6. Token stored in hidden input field
  7. AJAX collects token and sends to backend
  8. Backend validates token with Google API
  9. Search proceeds if validation passes

Turnstile Flow

  1. User loads page with checker shortcode
  2. load_captcha_scripts() enqueues Turnstile script
  3. Script renders visible widget in form
  4. User completes Turnstile challenge
  5. Token generated via callback and stored in hidden input
  6. User clicks search button
  7. AJAX collects token and sends to backend
  8. Backend validates token with Cloudflare API
  9. 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

  1. Token Caching: reCAPTCHA tokens are cached for 2 minutes to avoid excessive API calls when users click search multiple times quickly.

  2. Event Capture Phase: reCAPTCHA listener uses capture phase (true as third parameter) to ensure it runs before the main click handler.

  3. Turnstile Widget: Turnstile renders a visible widget that users must interact with, unlike reCAPTCHA v3 which is invisible.

  4. Backward Compatibility: All changes are backward compatible. Checkers without CAPTCHA enabled continue to work normally.

  5. Error Handling: Both CAPTCHA implementations include error callbacks that log to console without breaking the form.


Files Modified

  1. includes/class-Shortcode.php - Added CAPTCHA script loading, require tokens when enabled, added verification to both AJAX handlers
  2. assets/public.js - Added token collection and sending to both search button and loadAllData AJAX
  3. includes/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:

  1. Disable CAPTCHA requirement for "show all" mode (less secure)
  2. Show form first, require user interaction before loading data (more secure)
  3. 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 uses verify_all_security()
  • checker_load_all_data() now uses verify_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_load parameter 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 checkerSecurity localized 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_expired for frontend handling

Complete Feature Flow Verification

Search Button Click Flow

  1. User clicks search button
  2. Remove any existing security errors
  3. Check if reCAPTCHA token needs refresh → refresh if needed
  4. Build secure AJAX data with buildSecureAjaxData()
  5. Send AJAX to getAjaxUrl() (not hardcoded)
  6. Backend checks nonce status
  7. 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
  8. Return error with type and i18n message if any check fails
  9. Frontend handles error via handleAjaxError()
  10. Show user-friendly error with refresh button if nonce expired

Show All Mode Flow

  1. Page loads with checker
  2. CAPTCHA scripts load via load_captcha_scripts()
  3. loadAllData() called with isInitialLoad: true
  4. Build secure AJAX data (tokens may not be ready yet)
  5. Backend checks nonce
  6. Backend calls verify_all_security() with skip_captcha = true for initial load
  7. Still checks honeypot and rate limit
  8. Data loads successfully
  9. Subsequent searches require full CAPTCHA verification

Honeypot Flow

  1. Admin enables honeypot in security settings
  2. Shortcode renders invisible honeypot field
  3. Real users never see or fill it
  4. Bots auto-fill the field
  5. Frontend collects honeypot value in AJAX
  6. Backend checks honeypot first (fastest check)
  7. If filled → reject with generic error (don't reveal honeypot detection)

Nonce Expiry Flow

  1. User opens page → nonce created
  2. After 12-24 hours → nonce expires
  3. User tries to search
  4. Backend detects expired nonce
  5. Returns type: "nonce_expired" with i18n message
  6. Frontend shows error with "Refresh Page" button
  7. User clicks → page reloads with fresh nonce

Files Modified (Complete List)

  1. includes/class-Security.php - Added helper methods, unified verification, nonce status check
  2. includes/class-Shortcode.php - Refactored AJAX handlers, added honeypot, i18n localization
  3. assets/public.js - Added helper functions, error handling, token refresh, AJAX URL fix
  4. includes/helpers/class-Captcha-Helper.php - Fixed reCAPTCHA token generation timing
  5. templates/editor/setting-table-security.php - Added error message fields, honeypot settings
  6. admin/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