From bb5fce744a932083cfc9ea0f3ee1c87725825164 Mon Sep 17 00:00:00 2001 From: miyanialkesh7 Date: Sat, 3 Oct 2026 15:22:39 +0530 Subject: [PATCH] fix: escape custom-table names interpolated into raw SQL, add missing docblocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit esc_sql() the $wpdb->prefix-derived table names used in raw SQL strings across the custom-table queries (rate limiter, blocker, detector, WooCommerce tracking, violation/referral counters, admin reporting pages). Also adds docblocks to the constructors and methods that were missing them. Not changed, and why: - includes/class-webdecoy-violation-reporter.php's remaining occurrences: this file is required standalone (no WordPress) by tests/ViolationDrainTest.php, and esc_sql() is undefined outside WordPress. Verified by trying it: the suite breaks with "Call to undefined function esc_sql()". - $where/$query/$export_query in detections-page.php and blocker.php: these are already correctly parameterized through $wpdb->prepare(); Plugin Check's static analyzer just can't trace that a few layers up. Not a real gap. - 83 DirectQuery/NoCaching and 6 error_log() warnings: left as-is per prior discussion — caching would break rate-limit correctness, and the error_log() calls are legitimate exception-catch logging. --- admin/partials/detections-page.php | 2 +- admin/partials/statistics-page.php | 6 +++--- includes/class-webdecoy-actor-feed.php | 2 +- includes/class-webdecoy-actor-intel.php | 5 +++++ includes/class-webdecoy-ai-referrals.php | 12 ++++++++++- includes/class-webdecoy-blocker.php | 20 +++++++++---------- includes/class-webdecoy-cli.php | 6 +++--- includes/class-webdecoy-cloud-connect.php | 8 ++++++++ includes/class-webdecoy-cloud-policy.php | 3 +++ includes/class-webdecoy-decoy-response.php | 16 +++++++++++++++ includes/class-webdecoy-detector.php | 8 ++++---- includes/class-webdecoy-honeytoken.php | 5 +++++ includes/class-webdecoy-ip-enrichment.php | 5 +++++ includes/class-webdecoy-rate-limiter.php | 14 ++++++------- includes/class-webdecoy-updater.php | 3 +++ .../class-webdecoy-violation-reporter.php | 10 ++++++++++ includes/class-webdecoy-woocommerce.php | 14 ++++++------- webdecoy.php | 8 ++++++++ 18 files changed, 110 insertions(+), 37 deletions(-) diff --git a/admin/partials/detections-page.php b/admin/partials/detections-page.php index f91e450..9382e8b 100644 --- a/admin/partials/detections-page.php +++ b/admin/partials/detections-page.php @@ -17,7 +17,7 @@ global $wpdb; -$table = $wpdb->prefix . 'webdecoy_detections'; +$table = esc_sql($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; diff --git a/admin/partials/statistics-page.php b/admin/partials/statistics-page.php index a9516f7..a0c6e0b 100644 --- a/admin/partials/statistics-page.php +++ b/admin/partials/statistics-page.php @@ -16,9 +16,9 @@ global $wpdb; -$detections_table = $wpdb->prefix . 'webdecoy_detections'; -$blocked_table = $wpdb->prefix . 'webdecoy_blocked_ips'; -$checkout_table = $wpdb->prefix . 'webdecoy_checkout_attempts'; +$detections_table = esc_sql($wpdb->prefix . 'webdecoy_detections'); +$blocked_table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); +$checkout_table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); // 30-day detection trend $thirty_days_ago = gmdate('Y-m-d H:i:s', strtotime('-30 days')); diff --git a/includes/class-webdecoy-actor-feed.php b/includes/class-webdecoy-actor-feed.php index c4e5f09..366069d 100644 --- a/includes/class-webdecoy-actor-feed.php +++ b/includes/class-webdecoy-actor-feed.php @@ -312,7 +312,7 @@ public function intel_for(string $ip): ?array public static function purge_feed_blocks(): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $rows = $wpdb->get_col($wpdb->prepare( "SELECT ip_address FROM {$table} WHERE created_by = %s", self::CREATED_BY diff --git a/includes/class-webdecoy-actor-intel.php b/includes/class-webdecoy-actor-intel.php index 4dcc49d..5cbc789 100644 --- a/includes/class-webdecoy-actor-intel.php +++ b/includes/class-webdecoy-actor-intel.php @@ -49,6 +49,11 @@ class WebDecoy_Actor_Intel /** @var string Plaintext API key. */ private $apiKey; + /** + * Constructor + * + * @param string $apiKey WebDecoy Cloud API key + */ public function __construct(string $apiKey) { $this->apiKey = $apiKey; diff --git a/includes/class-webdecoy-ai-referrals.php b/includes/class-webdecoy-ai-referrals.php index af0fa75..d75cde7 100644 --- a/includes/class-webdecoy-ai-referrals.php +++ b/includes/class-webdecoy-ai-referrals.php @@ -31,10 +31,13 @@ class WebDecoy_AI_Referrals /** Distinct platform and path pairs kept before new ones are dropped. */ private const MAX_ROWS = 5000; + /** + * The ai_referrals table name, prefixed and escaped for interpolation. + */ public static function table(): string { global $wpdb; - return $wpdb->prefix . 'webdecoy_ai_referrals'; + return esc_sql($wpdb->prefix . 'webdecoy_ai_referrals'); } /** @@ -103,6 +106,13 @@ public static function landingPath(string $uri): string return mb_check_encoding($path, 'UTF-8') ? $path : '/'; } + /** + * Record one referral for a platform/landing-path pair, bounded to + * {@see self::MAX_ROWS} distinct pairs. + * + * @param string $platform Referring AI platform + * @param string $path Landing path the referral counted against + */ private static function increment(string $platform, string $path): void { global $wpdb; diff --git a/includes/class-webdecoy-blocker.php b/includes/class-webdecoy-blocker.php index 9803d46..6b37c8c 100644 --- a/includes/class-webdecoy-blocker.php +++ b/includes/class-webdecoy-blocker.php @@ -78,7 +78,7 @@ public function block(string $ip, string $reason = '', ?int $duration_hours = nu } } - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $expires_at = null; if ($duration_hours !== null && $duration_hours > 0) { @@ -261,7 +261,7 @@ public function unblock(string $ip): bool { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $result = $wpdb->delete($table, ['ip_address' => $ip]); @@ -292,7 +292,7 @@ public function is_blocked(string $ip): bool global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); // First check for exact IP match (fastest) $exact_blocked = $wpdb->get_var($wpdb->prepare( @@ -344,7 +344,7 @@ public function get_blocked_ips(array $args = []): array ]; $args = wp_parse_args($args, $defaults); - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $where = '1=1'; if (!$args['include_expired']) { @@ -375,7 +375,7 @@ public function get_blocked_count(bool $include_expired = false): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $where = '1=1'; if (!$include_expired) { @@ -395,7 +395,7 @@ public function get_block_info(string $ip): ?array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $result = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE ip_address = %s AND (expires_at IS NULL OR expires_at > %s)", @@ -417,7 +417,7 @@ public function extend_block(string $ip, int $hours): bool { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $info = $this->get_block_info($ip); if (!$info) { @@ -449,7 +449,7 @@ public function clear_all(): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $count = $wpdb->query("DELETE FROM {$table}"); @@ -470,7 +470,7 @@ public function cleanup_expired(): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); return $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE expires_at IS NOT NULL AND expires_at < %s", @@ -740,7 +740,7 @@ public function get_stats(): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_blocked_ips'; + $table = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $total = $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); $active = $wpdb->get_var($wpdb->prepare( diff --git a/includes/class-webdecoy-cli.php b/includes/class-webdecoy-cli.php index 045b18c..194d170 100644 --- a/includes/class-webdecoy-cli.php +++ b/includes/class-webdecoy-cli.php @@ -76,8 +76,8 @@ public function status($args, $assoc_args): void $mode .= ' [forced by WEBDECOY_DEFAULT_MODE]'; } - $detections = $wpdb->prefix . 'webdecoy_detections'; - $blocked = $wpdb->prefix . 'webdecoy_blocked_ips'; + $detections = esc_sql($wpdb->prefix . 'webdecoy_detections'); + $blocked = esc_sql($wpdb->prefix . 'webdecoy_blocked_ips'); $now = gmdate('Y-m-d H:i:s'); $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$detections}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix @@ -284,7 +284,7 @@ public function logs($args, $assoc_args): void \WP_CLI::confirm('Delete ALL recorded detections on this site?', $assoc_args); - $detections = $wpdb->prefix . 'webdecoy_detections'; + $detections = esc_sql($wpdb->prefix . 'webdecoy_detections'); $deleted = $wpdb->query("DELETE FROM {$detections}"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix \WP_CLI::success(sprintf('Deleted %d detection%s.', (int) $deleted, (int) $deleted === 1 ? '' : 's')); } diff --git a/includes/class-webdecoy-cloud-connect.php b/includes/class-webdecoy-cloud-connect.php index 4a8724a..a272aa6 100644 --- a/includes/class-webdecoy-cloud-connect.php +++ b/includes/class-webdecoy-cloud-connect.php @@ -555,6 +555,14 @@ public static function connected_notice_message(string $org): string return sprintf(__('Connected to WebDecoy Cloud (%s).', 'webdecoy'), $org) . ' ' . $tail; } + /** + * Stash an admin notice for display on the next page load. + * + * @param string $type Notice type (e.g. 'success', 'error') + * @param string $message Notice text + * @param string $url Optional action link URL + * @param string $label Optional action link label + */ private function set_notice(string $type, string $message, string $url = '', string $label = ''): void { set_transient( diff --git a/includes/class-webdecoy-cloud-policy.php b/includes/class-webdecoy-cloud-policy.php index 57200ca..6ab8633 100644 --- a/includes/class-webdecoy-cloud-policy.php +++ b/includes/class-webdecoy-cloud-policy.php @@ -174,6 +174,9 @@ public static function clear(): void delete_option(self::OPTION); } + /** + * The connected organization id from the stored plugin options, if any. + */ private static function organization_id(): string { $options = get_option('webdecoy_options', []); diff --git a/includes/class-webdecoy-decoy-response.php b/includes/class-webdecoy-decoy-response.php index a8666bb..e24d3f0 100644 --- a/includes/class-webdecoy-decoy-response.php +++ b/includes/class-webdecoy-decoy-response.php @@ -48,6 +48,12 @@ private static function secret(): string return $secret; } + /** + * Derive a stable per-label canary value from the site secret. + * + * @param string $label Canary label + * @param int $len Length of the returned hex string + */ private static function derive(string $label, int $len = 16): string { return substr(hash_hmac('sha256', $label, self::secret()), 0, $len); @@ -204,6 +210,9 @@ public function served_canaries(string $path): array return $this->decoy_for($path) === null ? [] : self::canaries(); } + /** + * Send a plain 404 response and stop execution. + */ private function serve_404(): void { nocache_headers(); @@ -213,6 +222,13 @@ private function serve_404(): void exit; } + /** + * Send a 200 response with the given body and content type, and stop + * execution. + * + * @param string $body Response body + * @param string $type Content-Type value + */ private function serve_body(string $body, string $type): void { nocache_headers(); diff --git a/includes/class-webdecoy-detector.php b/includes/class-webdecoy-detector.php index fc4f865..b60f5ab 100644 --- a/includes/class-webdecoy-detector.php +++ b/includes/class-webdecoy-detector.php @@ -236,7 +236,7 @@ public function log_detection(\WebDecoy\DetectionResult $result, string $ip): vo { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_detections'; + $table = esc_sql($wpdb->prefix . 'webdecoy_detections'); $wpdb->insert($table, [ 'ip_address' => $ip, @@ -271,7 +271,7 @@ public function get_recent_detections(int $limit = 10): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_detections'; + $table = esc_sql($wpdb->prefix . 'webdecoy_detections'); return $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d", @@ -289,7 +289,7 @@ public function get_stats(int $days = 7): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_detections'; + $table = esc_sql($wpdb->prefix . 'webdecoy_detections'); $since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $total = $wpdb->get_var($wpdb->prepare( @@ -331,7 +331,7 @@ public function get_unique_ips(int $days = 7): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_detections'; + $table = esc_sql($wpdb->prefix . 'webdecoy_detections'); $since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); return (int) $wpdb->get_var($wpdb->prepare( diff --git a/includes/class-webdecoy-honeytoken.php b/includes/class-webdecoy-honeytoken.php index 98bc7d8..1ff84ec 100644 --- a/includes/class-webdecoy-honeytoken.php +++ b/includes/class-webdecoy-honeytoken.php @@ -39,6 +39,11 @@ class WebDecoy_Honeytoken /** @var bool */ private $rotate; + /** + * Constructor + * + * @param bool $rotate Whether to rotate the per-site secret on next use + */ public function __construct(bool $rotate = false) { $this->rotate = $rotate; diff --git a/includes/class-webdecoy-ip-enrichment.php b/includes/class-webdecoy-ip-enrichment.php index 9d7d0e6..b50e3ac 100644 --- a/includes/class-webdecoy-ip-enrichment.php +++ b/includes/class-webdecoy-ip-enrichment.php @@ -35,6 +35,11 @@ class WebDecoy_IP_Enrichment /** @var int Request timeout in seconds (filterable). */ private $timeout; + /** + * Constructor + * + * @param string $apiKey WebDecoy Cloud API key + */ public function __construct(string $apiKey) { $this->apiKey = $apiKey; diff --git a/includes/class-webdecoy-rate-limiter.php b/includes/class-webdecoy-rate-limiter.php index 7e9bb18..0f809ee 100644 --- a/includes/class-webdecoy-rate-limiter.php +++ b/includes/class-webdecoy-rate-limiter.php @@ -84,7 +84,7 @@ public function check_and_increment(string $key): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $now = current_time('mysql', true); $window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); @@ -126,7 +126,7 @@ public function increment(string $ip): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $now = current_time('mysql', true); $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); @@ -168,7 +168,7 @@ public function get_count(string $ip): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $count = $wpdb->get_var($wpdb->prepare( @@ -202,7 +202,7 @@ public function get_reset_time(string $ip): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $window_start = $wpdb->get_var($wpdb->prepare( @@ -231,7 +231,7 @@ public function reset(string $ip): bool { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); return $wpdb->delete($table, ['ip_address' => $ip]) !== false; } @@ -245,7 +245,7 @@ public function cleanup(): int { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $threshold = gmdate('Y-m-d H:i:s', strtotime('-1 hour')); return $wpdb->query($wpdb->prepare( @@ -338,7 +338,7 @@ public function get_stats(): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_rate_limits'; + $table = esc_sql($wpdb->prefix . 'webdecoy_rate_limits'); $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $active_ips = $wpdb->get_var($wpdb->prepare( diff --git a/includes/class-webdecoy-updater.php b/includes/class-webdecoy-updater.php index 2af60e9..b6fd9fa 100644 --- a/includes/class-webdecoy-updater.php +++ b/includes/class-webdecoy-updater.php @@ -21,6 +21,9 @@ */ class WebDecoy_Updater { + /** + * Constructor + */ public function __construct() { add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']); diff --git a/includes/class-webdecoy-violation-reporter.php b/includes/class-webdecoy-violation-reporter.php index d6468ce..6d34010 100644 --- a/includes/class-webdecoy-violation-reporter.php +++ b/includes/class-webdecoy-violation-reporter.php @@ -62,6 +62,11 @@ class WebDecoy_Violation_Reporter /** @var bool Whether the shutdown drain has been registered. */ private $registered = false; + /** + * Constructor + * + * @param string $apiKey WebDecoy Cloud API key + */ public function __construct(string $apiKey) { $this->apiKey = $apiKey; @@ -176,6 +181,11 @@ private static function acquire_drain_lock(): bool return add_option(self::DRAIN_LOCK, (string) time(), '', 'no'); } + /** + * Send one batch of queued violations, assuming the drain lock is held. + * + * @param string $apiKey WebDecoy Cloud API key + */ private static function drain_locked(string $apiKey): void { global $wpdb; diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php index 5b79ae4..cf6602d 100644 --- a/includes/class-webdecoy-woocommerce.php +++ b/includes/class-webdecoy-woocommerce.php @@ -233,7 +233,7 @@ public function track_attempt(int $order_id): void global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); $wpdb->insert($table, [ 'ip_address' => $this->get_client_ip(), @@ -284,7 +284,7 @@ public function resolve_attempt(int $order_id, string $status): void { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); // Keyed on order_id ALONE, not on the current request's IP. Payment // completion often arrives on an asynchronous gateway callback (IPN, webhook, @@ -340,7 +340,7 @@ public function track_failure(int $order_id, string $reason = 'failed'): void { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); $ip = $this->get_client_ip(); // Determine status based on reason @@ -368,7 +368,7 @@ private function get_recent_attempts(string $ip, int $window, bool $include_succ { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); $since = gmdate('Y-m-d H:i:s', strtotime("-{$window} seconds")); if ($include_successful) { @@ -425,7 +425,7 @@ private function log_detection(string $ip, string $reason, ?int $score = null): { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_detections'; + $table = esc_sql($wpdb->prefix . 'webdecoy_detections'); $final_score = $score ?? 100; $source = 'woocommerce_' . $reason; @@ -576,7 +576,7 @@ public function get_stats(int $days = 7): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); $since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $total = $wpdb->get_var($wpdb->prepare( @@ -622,7 +622,7 @@ public function get_suspicious_ips(int $threshold = 3): array { global $wpdb; - $table = $wpdb->prefix . 'webdecoy_checkout_attempts'; + $table = esc_sql($wpdb->prefix . 'webdecoy_checkout_attempts'); $since = gmdate('Y-m-d H:i:s', strtotime('-24 hours')); return $wpdb->get_results($wpdb->prepare( diff --git a/webdecoy.php b/webdecoy.php index 47bd734..316fc93 100644 --- a/webdecoy.php +++ b/webdecoy.php @@ -1576,6 +1576,14 @@ private function record_suppressed_action(string $suppression, string $reason): unset($suppression); } + /** + * Carry out the action the rule engine decided on for this request + * (THROTTLE, deceptive response, IP block, or 403), unless enforcement is + * currently suppressed. + * + * @param \WebDecoy\Rules\RuleEngineResult $result Rule engine decision + * @param string $ip Request IP address + */ private function handle_rule_decision(\WebDecoy\Rules\RuleEngineResult $result, string $ip): void { // Monitor mode, the kill switch, or an unconfigured proxy: the violation has