- class-itk-whitelist.php: static class with 5min transient cache, supports exact IP, CIDR notation, and ua: prefix for UA substrings - config/whitelist.conf: editable config file (template with examples) - whitelist check added to bot-blocker, WAF, protection (4 methods), and honeypot validator — matched requests skip all ITK enforcement - admin: whitelist.conf added to Config Files editor tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
283 lines
11 KiB
PHP
283 lines
11 KiB
PHP
<?php
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
/**
|
||
* ITK Honeypot
|
||
*
|
||
* Ported from HoneypotFields v2.4.0.
|
||
* Injects invisible honeypot fields into all major form types and blocks
|
||
* submissions that fill them (bots) or arrive too fast/too slow.
|
||
* Uses ITK_Database for logging instead of a separate table.
|
||
*/
|
||
class ITK_Honeypot {
|
||
|
||
const FIELD_PREFIX = '_hp_trap_';
|
||
const TOKEN_FIELD = '_hp_token';
|
||
const TIME_FIELD = '_hp_ts';
|
||
|
||
private static array $opts = [];
|
||
|
||
public function __construct() {
|
||
self::$opts = get_option('itk_honeypot', []);
|
||
|
||
if (empty(self::$opts['enabled'])) return;
|
||
|
||
// Inject honeypot into forms
|
||
add_action('comment_form', [$this, 'inject_comment']);
|
||
add_action('login_form', [$this, 'inject_generic']);
|
||
add_action('register_form', [$this, 'inject_generic']);
|
||
add_action('lostpassword_form', [$this, 'inject_generic']);
|
||
add_action('wp_head', [$this, 'inject_honeypot_style']);
|
||
|
||
// Validate on submission
|
||
add_filter('preprocess_comment', [$this, 'validate_comment'], 1);
|
||
add_action('authenticate', [$this, 'validate_login'], 1, 3);
|
||
add_action('register_post', [$this, 'validate_register'], 1, 3);
|
||
add_action('lostpassword_post', [$this, 'validate_lost_password'], 1);
|
||
|
||
// WooCommerce
|
||
if (!empty(self::$opts['protect_woocommerce']) && class_exists('WooCommerce')) {
|
||
add_action('woocommerce_checkout_before_customer_details', [$this, 'inject_generic']);
|
||
add_action('woocommerce_checkout_process', [$this, 'validate_woo_checkout']);
|
||
add_action('woocommerce_register_form', [$this, 'inject_generic']);
|
||
add_action('woocommerce_process_registration_errors', [$this, 'validate_woo_registration'], 10, 3);
|
||
add_action('woocommerce_login_form', [$this, 'inject_generic']);
|
||
}
|
||
|
||
// Contact Form 7
|
||
if (!empty(self::$opts['protect_cf7']) && class_exists('WPCF7')) {
|
||
add_action('wpcf7_form_elements', [$this, 'inject_cf7']);
|
||
add_filter('wpcf7_before_send_mail', [$this, 'validate_cf7'], 10, 3);
|
||
}
|
||
|
||
// Elementor forms
|
||
if (!empty(self::$opts['protect_elementor']) && defined('ELEMENTOR_VERSION')) {
|
||
add_action('elementor/frontend/after_enqueue_scripts', [$this, 'elementor_enqueue']);
|
||
add_action('elementor_pro/forms/validation', [$this, 'validate_elementor'], 10, 2);
|
||
}
|
||
|
||
// Gravity Forms
|
||
if (!empty(self::$opts['protect_gravity']) && class_exists('GFForms')) {
|
||
add_filter('gform_form_tag', [$this, 'inject_gravity'], 10, 2);
|
||
add_filter('gform_validation', [$this, 'validate_gravity']);
|
||
}
|
||
|
||
// Search form
|
||
if (!empty(self::$opts['protect_search'])) {
|
||
add_action('get_search_form', [$this, 'inject_search']);
|
||
}
|
||
|
||
// Enqueue JS token generator
|
||
add_action('wp_enqueue_scripts', [$this, 'enqueue_token_script']);
|
||
}
|
||
|
||
/* ── Style (hide honeypot fields) ─────────────────────────── */
|
||
|
||
public function inject_honeypot_style(): void {
|
||
echo '<style>.itk-hp-field{display:none!important;visibility:hidden!important;opacity:0!important;position:absolute!important;left:-9999px!important;top:-9999px!important;}</style>' . "\n";
|
||
}
|
||
|
||
/* ── JS token (HMAC-based anti-CSRF) ─────────────────────── */
|
||
|
||
public function enqueue_token_script(): void {
|
||
$secret = $this->get_page_secret();
|
||
wp_add_inline_script('jquery-core', "
|
||
(function(){
|
||
var s='" . esc_js($secret) . "',n='" . esc_js(wp_create_nonce('itk_hp')) . "';
|
||
document.querySelectorAll('." . self::TOKEN_FIELD . "').forEach(function(f){
|
||
f.value=btoa(s+'|'+Date.now()+'|'+n);
|
||
});
|
||
document.querySelectorAll('." . self::TIME_FIELD . "').forEach(function(f){
|
||
f.value=Math.floor(Date.now()/1000);
|
||
});
|
||
})();
|
||
", 'after');
|
||
}
|
||
|
||
/* ── Field HTML generator ─────────────────────────────────── */
|
||
|
||
private function honeypot_html(string $form_type = ''): string {
|
||
$field = self::FIELD_PREFIX . substr(md5(uniqid()), 0, 8);
|
||
$ts = time();
|
||
$label = ['Your email address', 'Website URL', 'Full name'][array_rand(['a','b','c'])];
|
||
return sprintf(
|
||
'<div class="itk-hp-field" aria-hidden="true" tabindex="-1" style="display:none!important">
|
||
<label>%s <input type="text" name="%s" value="" autocomplete="off" tabindex="-1"></label>
|
||
<input type="hidden" name="%s" class="%s" value="">
|
||
<input type="hidden" name="%s" class="%s" value="%d">
|
||
</div>',
|
||
esc_html($label),
|
||
esc_attr($field),
|
||
self::TOKEN_FIELD, self::TOKEN_FIELD,
|
||
self::TIME_FIELD, self::TIME_FIELD,
|
||
$ts
|
||
);
|
||
}
|
||
|
||
/* ── Injectors ────────────────────────────────────────────── */
|
||
|
||
public function inject_generic(): void {
|
||
if (empty(self::$opts['enabled'])) return;
|
||
echo $this->honeypot_html(); // phpcs:ignore
|
||
}
|
||
|
||
public function inject_comment(): void {
|
||
if (empty(self::$opts['protect_comments'])) return;
|
||
echo $this->honeypot_html('comment'); // phpcs:ignore
|
||
}
|
||
|
||
public function inject_search(string $form): string {
|
||
return $form . $this->honeypot_html('search');
|
||
}
|
||
|
||
public function inject_cf7(string $content): string {
|
||
return $content . $this->honeypot_html('cf7');
|
||
}
|
||
|
||
public function inject_gravity(string $tag, array $form): string {
|
||
return $tag . $this->honeypot_html('gravity');
|
||
}
|
||
|
||
public function elementor_enqueue(): void {
|
||
// Elementor injects via JS – add hidden fields via wp_footer
|
||
add_action('wp_footer', [$this, 'inject_generic']);
|
||
}
|
||
|
||
/* ── Validators ───────────────────────────────────────────── */
|
||
|
||
private function check_honeypot(string $form_type): bool {
|
||
// Whitelisted IPs/UAs always pass honeypot validation.
|
||
if (ITK_Whitelist::allowed()) return true;
|
||
|
||
// 1. Honeypot field must be empty
|
||
foreach ($_POST as $key => $val) {
|
||
if (strpos($key, self::FIELD_PREFIX) === 0 && !empty($val)) {
|
||
$this->log_block($form_type, 'Honeypot field filled');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 2. Timing check
|
||
$opts = get_option('itk_honeypot', []);
|
||
$min_t = max(1, (int)($opts['min_time'] ?? 3));
|
||
$max_t = max(60, (int)($opts['max_time'] ?? 7200));
|
||
$ts = (int)($_POST[self::TIME_FIELD] ?? 0);
|
||
$elapsed = time() - $ts;
|
||
|
||
if ($ts > 0 && $elapsed < $min_t) {
|
||
$this->log_block($form_type, "Submitted too fast ({$elapsed}s)");
|
||
return false;
|
||
}
|
||
if ($ts > 0 && $elapsed > $max_t) {
|
||
$this->log_block($form_type, "Submitted too slow ({$elapsed}s > {$max_t}s)");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private function log_block(string $form_type, string $reason): void {
|
||
$event = [
|
||
'ip' => $this->get_ip(),
|
||
'form' => $form_type,
|
||
'reason' => $reason,
|
||
'uri' => $_SERVER['REQUEST_URI'] ?? '',
|
||
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||
];
|
||
ITK_Database::log_honeypot($event);
|
||
ITK_HP_API::queue($event);
|
||
}
|
||
|
||
public function validate_comment(array $comment_data): array {
|
||
if (empty(self::$opts['protect_comments'])) return $comment_data;
|
||
if (!$this->check_honeypot('comment')) {
|
||
wp_die('Spam detected. Please go back and try again.', 'Spam Blocked', ['response' => 403]);
|
||
}
|
||
return $comment_data;
|
||
}
|
||
|
||
public function validate_login($user, string $username, string $password) {
|
||
if (empty(self::$opts['protect_login'])) return $user;
|
||
if (!empty($username) && !$this->check_honeypot('login')) {
|
||
return new WP_Error('honeypot_blocked', 'Access denied.');
|
||
}
|
||
return $user;
|
||
}
|
||
|
||
public function validate_register($sanitized_user_login, $user_email, \WP_Error $errors): void {
|
||
if (empty(self::$opts['protect_register'])) return;
|
||
if (!$this->check_honeypot('register')) {
|
||
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||
}
|
||
}
|
||
|
||
public function validate_lost_password(\WP_Error $errors): void {
|
||
if (empty(self::$opts['protect_lost_password'])) return;
|
||
if (!$this->check_honeypot('lostpassword')) {
|
||
$errors->add('honeypot_blocked', 'Access denied.');
|
||
}
|
||
}
|
||
|
||
public function validate_woo_checkout(): void {
|
||
if (!$this->check_honeypot('woo_checkout')) {
|
||
wc_add_notice('Spam submission detected. Please refresh and try again.', 'error');
|
||
}
|
||
}
|
||
|
||
public function validate_woo_registration(\WP_Error $errors, $username, $email): \WP_Error {
|
||
if (!$this->check_honeypot('woo_register')) {
|
||
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||
}
|
||
return $errors;
|
||
}
|
||
|
||
public function validate_cf7($contact_form, &$abort, $submission): void {
|
||
if (!$this->check_honeypot('cf7')) {
|
||
$abort = true;
|
||
$contact_form->set_status('spam');
|
||
}
|
||
}
|
||
|
||
public function validate_elementor($record, $ajax_handler): void {
|
||
if (!$this->check_honeypot('elementor')) {
|
||
$ajax_handler->add_error_message('Spam detected. Please try again.');
|
||
}
|
||
}
|
||
|
||
public function validate_gravity(array $validation_result): array {
|
||
if (!$this->check_honeypot('gravity')) {
|
||
$validation_result['is_valid'] = false;
|
||
foreach ($validation_result['form']['fields'] as &$field) {
|
||
$field->failed_validation = true;
|
||
$field->validation_message = 'Spam detected.';
|
||
}
|
||
}
|
||
return $validation_result;
|
||
}
|
||
|
||
/* ── Helpers ──────────────────────────────────────────────── */
|
||
|
||
private function get_ip(): string {
|
||
$keys = [
|
||
'HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED',
|
||
'HTTP_X_CLUSTER_CLIENT_IP','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR',
|
||
];
|
||
foreach ($keys as $k) {
|
||
if (!empty($_SERVER[$k])) {
|
||
$ip = trim(explode(',', $_SERVER[$k])[0]);
|
||
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
|
||
}
|
||
}
|
||
return 'UNKNOWN';
|
||
}
|
||
|
||
private function get_page_secret(): string {
|
||
$secret = get_option('itk_hp_secret');
|
||
if (!$secret) {
|
||
$secret = wp_generate_password(32, false);
|
||
update_option('itk_hp_secret', $secret);
|
||
}
|
||
return $secret;
|
||
}
|
||
}
|