diff --git a/admin/partials/blocked-ips-page.php b/admin/partials/blocked-ips-page.php
index 2c64c36..401eef6 100644
--- a/admin/partials/blocked-ips-page.php
+++ b/admin/partials/blocked-ips-page.php
@@ -19,8 +19,9 @@
// Handle actions
if (isset($_POST['webdecoy_block_ip']) && isset($_POST['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'webdecoy_block_ip') && current_user_can('manage_options')) {
- $ip = sanitize_text_field($_POST['ip_address']);
- $reason = sanitize_text_field($_POST['reason'] ?? '');
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- presence check only; the value is validated below after nonce and capability checks
+ $ip = sanitize_text_field(wp_unslash($_POST['ip_address']));
+ $reason = sanitize_text_field(wp_unslash($_POST['reason'] ?? ''));
$duration = intval($_POST['duration'] ?? 24);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
diff --git a/admin/partials/detections-page.php b/admin/partials/detections-page.php
index f2136c4..564f508 100644
--- a/admin/partials/detections-page.php
+++ b/admin/partials/detections-page.php
@@ -18,18 +18,19 @@
global $wpdb;
$table = $wpdb->prefix . 'webdecoy_detections';
+// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only admin list filtering via GET, no state change
$page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$per_page = 50;
$offset = ($page - 1) * $per_page;
// Filters
-$threat_level = isset($_GET['threat_level']) ? sanitize_text_field($_GET['threat_level']) : '';
-$source = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : '';
+$threat_level = isset($_GET['threat_level']) ? sanitize_text_field(wp_unslash($_GET['threat_level'])) : '';
+$source = isset($_GET['source']) ? sanitize_text_field(wp_unslash($_GET['source'])) : '';
// Date range handling
-$date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : '';
-$date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : '';
-$range = isset($_GET['range']) ? sanitize_text_field($_GET['range']) : '';
+$date_from = isset($_GET['date_from']) ? sanitize_text_field(wp_unslash($_GET['date_from'])) : '';
+$date_to = isset($_GET['date_to']) ? sanitize_text_field(wp_unslash($_GET['date_to'])) : '';
+$range = isset($_GET['range']) ? sanitize_text_field(wp_unslash($_GET['range'])) : '';
if ($range === 'today') {
$date_from = gmdate('Y-m-d');
@@ -68,7 +69,7 @@
$where = implode(' AND ', $where_clauses);
// Handle CSV export
-if (isset($_GET['action']) && $_GET['action'] === 'export_csv' && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'webdecoy_export')) {
+if (isset($_GET['action']) && sanitize_text_field(wp_unslash($_GET['action'])) === 'export_csv' && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'webdecoy_export')) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="webdecoy-detections-' . gmdate('Y-m-d') . '.csv"');
@@ -157,7 +158,7 @@
if (!empty($_GET['date_from']) || !empty($_GET['date_to'])) {
$active_range = 'custom';
} elseif (isset($_GET['range'])) {
- $active_range = sanitize_text_field($_GET['range']);
+ $active_range = sanitize_text_field(wp_unslash($_GET['range']));
}
?>
get_results($wpdb->prepare(
- "SELECT DATE(created_at) as date, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY DATE(created_at) ORDER BY date ASC",
+ "SELECT DATE(created_at) as date, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY DATE(created_at) ORDER BY date ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
@@ -41,13 +41,13 @@
// Threat level distribution
$threat_distribution = $wpdb->get_results($wpdb->prepare(
- "SELECT threat_level, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY threat_level ORDER BY count DESC",
+ "SELECT threat_level, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY threat_level ORDER BY count DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
// Top flagged signals (parse from JSON flags)
$recent_flags = $wpdb->get_results($wpdb->prepare(
- "SELECT flags FROM {$detections_table} WHERE created_at > %s AND flags IS NOT NULL AND flags != '' LIMIT 500",
+ "SELECT flags FROM {$detections_table} WHERE created_at > %s AND flags IS NOT NULL AND flags != '' LIMIT 500", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
@@ -78,29 +78,29 @@
// Top blocked IPs
$top_ips = $wpdb->get_results($wpdb->prepare(
- "SELECT ip_address, COUNT(*) as count, MAX(created_at) as last_seen FROM {$detections_table} WHERE created_at > %s GROUP BY ip_address ORDER BY count DESC LIMIT 10",
+ "SELECT ip_address, COUNT(*) as count, MAX(created_at) as last_seen FROM {$detections_table} WHERE created_at > %s GROUP BY ip_address ORDER BY count DESC LIMIT 10", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
// Source distribution
$source_distribution = $wpdb->get_results($wpdb->prepare(
- "SELECT source, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY source ORDER BY count DESC",
+ "SELECT source, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY source ORDER BY count DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
), ARRAY_A);
// Overall stats
$total_30d = array_sum($daily_data);
$total_7d = $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s",
+ "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
gmdate('Y-m-d H:i:s', strtotime('-7 days'))
));
$total_today = $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s",
+ "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
gmdate('Y-m-d 00:00:00')
));
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- static query, no user input
$active_blocks = $wpdb->get_var(
- "SELECT COUNT(*) FROM {$blocked_table} WHERE expires_at IS NULL OR expires_at > NOW()"
+ "SELECT COUNT(*) FROM {$blocked_table} WHERE expires_at IS NULL OR expires_at > NOW()" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
);
// WooCommerce stats (if active)
@@ -108,11 +108,11 @@
if (class_exists('WooCommerce')) {
$woo_stats = [
'attempts' => (int) $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$checkout_table} WHERE created_at > %s",
+ "SELECT COUNT(*) FROM {$checkout_table} WHERE created_at > %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
)),
'blocked' => (int) $wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$checkout_table} WHERE created_at > %s AND status = 'blocked'",
+ "SELECT COUNT(*) FROM {$checkout_table} WHERE created_at > %s AND status = 'blocked'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$thirty_days_ago
)),
];
diff --git a/cdn-files/plugin-info.json b/cdn-files/plugin-info.json
index efc240f..02b69a5 100644
--- a/cdn-files/plugin-info.json
+++ b/cdn-files/plugin-info.json
@@ -1,13 +1,13 @@
{
"name": "WebDecoy Bot Detection",
"slug": "webdecoy",
- "version": "2.2.1",
+ "version": "2.2.2",
"author": "WebDecoy",
"author_profile": "https://webdecoy.com",
"requires": "5.6",
"tested": "6.7",
"requires_php": "7.4",
- "download_url": "https://cdn.webdecoy.com/wordpress/webdecoy-2.2.1.zip",
+ "download_url": "https://cdn.webdecoy.com/wordpress/webdecoy-2.2.2.zip",
"sections": {
"description": "
WebDecoy provides enterprise-grade bot detection and fraud protection for WordPress websites. Unlike simple CAPTCHA solutions, WebDecoy uses a layered defense approach that analyzes visitors from multiple angles — including deterministic tripwires that catch scanners with zero false positives.
Key Features
- Deterministic tripwires (hidden honeypot paths) — zero-false-positive bot blocking
- Server-side and client-side bot detection
- Invisible proof-of-work challenge (no external CAPTCHA service)
- Comment, login, and registration spam protection
- WooCommerce carding attack prevention
- 60+ good bots automatically allowed
- AI crawler detection and blocking
- Optional WebDecoy Cloud: centralized dashboard and rotation-proof device lockouts
",
"installation": "- Upload the plugin files to
/wp-content/plugins/webdecoy - Activate the plugin through the Plugins menu
- Tripwires and local protection are active out of the box — no API key required
- Optionally go to WebDecoy > Settings > WebDecoy Cloud to connect for centralized monitoring and enforcement
",
diff --git a/changelog.txt b/changelog.txt
index c01ab90..5595a52 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,12 @@
*** WebDecoy Bot Detection Changelog ***
+= 2.2.2 - 2026-07-20 =
+* Fixed: A PHP 7.4 fatal error in good-bot verification (str_ends_with is PHP 8.0+)
+* Fixed: Timezone-safe date handling throughout (date() -> gmdate())
+* Changed: The bundled SDK now uses the WordPress HTTP API exclusively (removed the raw cURL fallback)
+* Changed: Requires WordPress 6.1+ (wp_cache_flush_group); tested up to WordPress 7.0
+* Internal: Full WordPress Plugin Check compliance — resolved all 69 flagged errors
+
= 2.2.1 - 2026-07-19 =
* Changed: Chart.js is now bundled with the plugin instead of loaded from a CDN — no external requests for the admin charts
* Changed: Clarified the External Services disclosure in readme
diff --git a/includes/class-webdecoy-detector.php b/includes/class-webdecoy-detector.php
index 02dd1c0..7a2209d 100644
--- a/includes/class-webdecoy-detector.php
+++ b/includes/class-webdecoy-detector.php
@@ -167,9 +167,9 @@ private function is_login_page(): bool
*/
private function is_register_page(): bool
{
- return ($GLOBALS['pagenow'] ?? '') === 'wp-login.php' &&
- isset($_REQUEST['action']) &&
- $_REQUEST['action'] === 'register';
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page detection, no state change
+ $action = isset($_REQUEST['action']) ? sanitize_text_field(wp_unslash($_REQUEST['action'])) : '';
+ return ($GLOBALS['pagenow'] ?? '') === 'wp-login.php' && $action === 'register';
}
/**
diff --git a/includes/class-webdecoy-pow.php b/includes/class-webdecoy-pow.php
index c99223e..0e315aa 100644
--- a/includes/class-webdecoy-pow.php
+++ b/includes/class-webdecoy-pow.php
@@ -146,7 +146,7 @@ public function get_difficulty_for_ip( $ip, $base = 4 ) {
}
// Check for known bot UA patterns
- $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
+ $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
if ( $this->is_suspicious_ua( $ua ) ) {
$difficulty++;
}
diff --git a/includes/class-webdecoy-violation-reporter.php b/includes/class-webdecoy-violation-reporter.php
index 74968c5..7e5f1bf 100644
--- a/includes/class-webdecoy-violation-reporter.php
+++ b/includes/class-webdecoy-violation-reporter.php
@@ -140,7 +140,7 @@ public static function drain_queue(string $apiKey): void
// Enforce the hard cap first: discard the oldest overflow so a prolonged
// ingest outage can't grow the table without bound.
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix, not user input
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
if ($total > self::MAX_QUEUE) {
$overflow = $total - self::MAX_QUEUE;
diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php
index ac1d72b..e3b7f70 100644
--- a/includes/class-webdecoy-woocommerce.php
+++ b/includes/class-webdecoy-woocommerce.php
@@ -340,7 +340,7 @@ private function log_detection(string $ip, string $reason, ?int $score = null):
// Log locally
$wpdb->insert($table, [
'ip_address' => $ip,
- 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
+ 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',
'score' => $final_score,
'threat_level' => 'HIGH',
'source' => $source,
diff --git a/readme.txt b/readme.txt
index 40157d4..5732ca2 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,12 +4,12 @@ Donate link: https://webdecoy.com
Tags: security, bot detection, spam protection, woocommerce, firewall
Requires at least: 6.1
Tested up to: 7.0
-Stable tag: 2.2.1
+Stable tag: 2.2.2
Requires PHP: 7.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
-Zero-configuration bot protection for WordPress. Works immediately on activation — no account, no API key, no external dependencies. Multi-layer detection with invisible proof-of-work challenges.
+Zero-config bot, spam, and carding protection. Works instantly on activation, with no account, API key, or external connections.
== Description ==
@@ -173,6 +173,9 @@ Privacy Policy: https://webdecoy.com/privacy
**Without an API key, the plugin operates 100% locally — no external connections on the front end or back end.** Chart.js (used for the admin Statistics charts) is bundled with the plugin, not loaded from a CDN.
+= Bundled third-party libraries =
+Chart.js v4.4.9 (MIT license) is included at admin/js/vendor/chart.umd.min.js for the admin Statistics charts. It is the official distribution build; the human-readable source is available at https://github.com/chartjs/Chart.js/releases/tag/v4.4.9 . No other third-party libraries are bundled.
+
== Screenshots ==
1. Protection settings — configure detection and blocking
@@ -186,6 +189,13 @@ Privacy Policy: https://webdecoy.com/privacy
== Changelog ==
+= 2.2.2 =
+* Fixed: A PHP 7.4 fatal error in good-bot verification (use of a PHP 8 function)
+* Fixed: Timezone-safe date handling throughout
+* Changed: The bundled SDK now uses the WordPress HTTP API exclusively (removed the raw cURL fallback)
+* Changed: Requires WordPress 6.1+; tested up to WordPress 7.0
+* Internal: Full WordPress Plugin Check compliance (resolved 69 flagged items)
+
= 2.2.1 =
* Changed: Chart.js (admin Statistics charts) is now bundled with the plugin instead of loaded from a CDN — the plugin makes no external requests until you connect a WebDecoy Cloud account
* Changed: Clarified the External Services disclosure
diff --git a/sdk/src/BotDetector.php b/sdk/src/BotDetector.php
index d38d889..424c4aa 100644
--- a/sdk/src/BotDetector.php
+++ b/sdk/src/BotDetector.php
@@ -265,7 +265,7 @@ private function getClientIP(): string
foreach ($headers as $header) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- IP validated with FILTER_VALIDATE_IP below
if (!empty($_SERVER[$header])) {
- // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- WP path unslashes + sanitizes; the standalone fallback trims the raw value because wp_unslash() is unavailable outside WordPress
$ip = function_exists('sanitize_text_field') ? sanitize_text_field(wp_unslash($_SERVER[$header])) : trim($_SERVER[$header]);
// X-Forwarded-For can contain multiple IPs
if (strpos($ip, ',') !== false) {
diff --git a/sdk/src/SignalCollector.php b/sdk/src/SignalCollector.php
index a87a5b4..7ad253e 100644
--- a/sdk/src/SignalCollector.php
+++ b/sdk/src/SignalCollector.php
@@ -82,7 +82,7 @@ private function getServerVar(string $key, ?string $default = null): ?string
return $default;
}
- // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- WP path unslashes + sanitizes below; the standalone fallback trims the raw value because wp_unslash() is unavailable outside WordPress
$value = $_SERVER[$key];
// Use WordPress sanitization if available
diff --git a/webdecoy.php b/webdecoy.php
index 7f692a0..504d85f 100644
--- a/webdecoy.php
+++ b/webdecoy.php
@@ -3,7 +3,7 @@
* Plugin Name: WebDecoy Bot Detection
* Plugin URI: https://webdecoy.com/wordpress
* Description: Protect your WordPress site from bots, spam, and carding attacks with WebDecoy's advanced threat detection.
- * Version: 2.2.1
+ * Version: 2.2.2
* Requires at least: 6.1
* Requires PHP: 7.4
* Author: WebDecoy
@@ -41,7 +41,7 @@ function str_starts_with(string $haystack, string $needle): bool
}
// Plugin constants
-define('WEBDECOY_VERSION', '2.2.1');
+define('WEBDECOY_VERSION', '2.2.2');
define('WEBDECOY_PLUGIN_FILE', __FILE__);
define('WEBDECOY_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WEBDECOY_PLUGIN_URL', plugin_dir_url(__FILE__));
@@ -1109,13 +1109,13 @@ private function build_rule_context(string $ip): \WebDecoy\Rules\RuleContext
? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT']))
: '';
- // Headers rules may read: Cookie (wd_clearance — passed raw so the token
- // value survives intact for forwarding) and any header referenced via
- // req.header("name"). We forward the full request header set for the
- // latter, plus the raw cookie.
+ // Headers rules may read: Cookie (wd_clearance — the token's base64url/JWT
+ // characters pass through sanitize_text_field intact for forwarding) and
+ // any header referenced via req.header("name"). We forward the full request
+ // header set for the latter, plus the sanitized cookie header.
$headers = $this->collect_request_headers();
if (isset($_SERVER['HTTP_COOKIE'])) {
- $headers['cookie'] = (string) wp_unslash($_SERVER['HTTP_COOKIE']);
+ $headers['cookie'] = sanitize_text_field(wp_unslash($_SERVER['HTTP_COOKIE']));
}
// Fetch IP enrichment only when a filter rule needs it (premium only).
@@ -1342,7 +1342,7 @@ private function handle_blocking(\WebDecoy\DetectionResult $result, string $ip):
*/
private function is_challenge_verified(string $ip): bool
{
- $cookie = isset($_COOKIE['webdecoy_verified']) ? sanitize_text_field($_COOKIE['webdecoy_verified']) : '';
+ $cookie = isset($_COOKIE['webdecoy_verified']) ? sanitize_text_field(wp_unslash($_COOKIE['webdecoy_verified'])) : '';
if (empty($cookie)) {
return false;
}
@@ -1418,7 +1418,7 @@ private function log_detection(\WebDecoy\DetectionResult $result, string $ip): v
$wpdb->insert($table, [
'ip_address' => $ip,
- 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
+ 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',
'score' => $result->getScore(),
'threat_level' => $result->getThreatLevel(),
'source' => 'wordpress_plugin',
@@ -1648,6 +1648,7 @@ private function check_honeypot(string $context): void
{
$honeypot_name = 'webdecoy_hp_' . $context;
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- honeypot detection: any submitted value is treated as a bot signal, so nonce verification is not applicable
if (isset($_POST[$honeypot_name]) && !empty($_POST[$honeypot_name])) {
// Honeypot triggered - definitely a bot
$ip = $this->get_client_ip();
@@ -2122,7 +2123,7 @@ public function ajax_client_detection(): void
}
// Get detection data
- $detection_json = isset($_POST['detection']) ? wp_unslash($_POST['detection']) : '';
+ $detection_json = isset($_POST['detection']) ? sanitize_text_field(wp_unslash($_POST['detection'])) : '';
$detection = json_decode($detection_json, true);
if (!$detection || !is_array($detection)) {
@@ -2294,7 +2295,7 @@ public function ajax_test_connection(): void
return;
}
- $api_key = sanitize_text_field($_POST['api_key'] ?? '');
+ $api_key = sanitize_text_field(wp_unslash($_POST['api_key'] ?? ''));
// Decrypt if encrypted
if (!empty($api_key) && $this->is_encrypted($api_key)) {
@@ -2369,8 +2370,8 @@ public function ajax_block_ip(): void
return;
}
- $ip = sanitize_text_field($_POST['ip'] ?? '');
- $reason = sanitize_text_field($_POST['reason'] ?? '');
+ $ip = sanitize_text_field(wp_unslash($_POST['ip'] ?? ''));
+ $reason = sanitize_text_field(wp_unslash($_POST['reason'] ?? ''));
$duration = intval($_POST['duration'] ?? 24);
if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
@@ -2396,7 +2397,7 @@ public function ajax_unblock_ip(): void
return;
}
- $ip = sanitize_text_field($_POST['ip'] ?? '');
+ $ip = sanitize_text_field(wp_unslash($_POST['ip'] ?? ''));
if (empty($ip)) {
wp_send_json_error(['message' => __('Invalid IP address.', 'webdecoy')]);
@@ -2421,7 +2422,7 @@ public function ajax_bulk_block(): void
return;
}
- $ips = isset($_POST['ips']) ? array_map('sanitize_text_field', (array) $_POST['ips']) : [];
+ $ips = isset($_POST['ips']) ? array_map('sanitize_text_field', wp_unslash((array) $_POST['ips'])) : [];
if (empty($ips)) {
wp_send_json_error(['message' => __('No IPs selected.', 'webdecoy')]);
@@ -2484,7 +2485,7 @@ public function ajax_pow_verify(): void
$ip = $this->get_client_ip();
// Get challenge data
- $challenge_json = isset($_POST['challenge']) ? wp_unslash($_POST['challenge']) : '';
+ $challenge_json = isset($_POST['challenge']) ? sanitize_text_field(wp_unslash($_POST['challenge'])) : '';
$challenge = json_decode($challenge_json, true);
if (!$challenge || !is_array($challenge)) {
@@ -2493,7 +2494,7 @@ public function ajax_pow_verify(): void
}
$nonce = isset($_POST['pow_nonce']) ? intval($_POST['pow_nonce']) : 0;
- $hash = isset($_POST['pow_hash']) ? wp_unslash($_POST['pow_hash']) : '';
+ $hash = isset($_POST['pow_hash']) ? sanitize_text_field(wp_unslash($_POST['pow_hash'])) : '';
if (!preg_match('/^[0-9a-f]{64}$/i', $hash)) {
wp_send_json_error(['message' => __('Invalid hash.', 'webdecoy')]);
return;
@@ -2509,7 +2510,7 @@ public function ajax_pow_verify(): void
}
// Score behavioral signals if provided
- $behavioral_json = isset($_POST['behavioral']) ? wp_unslash($_POST['behavioral']) : '';
+ $behavioral_json = isset($_POST['behavioral']) ? sanitize_text_field(wp_unslash($_POST['behavioral'])) : '';
$behavioral = json_decode($behavioral_json, true);
if ($behavioral && is_array($behavioral)) {