Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions admin/partials/detections-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@
$range = isset($_GET['range']) ? sanitize_text_field($_GET['range']) : '';

if ($range === 'today') {
$date_from = date('Y-m-d');
$date_to = date('Y-m-d');
$date_from = gmdate('Y-m-d');
$date_to = gmdate('Y-m-d');
} elseif ($range === '7d') {
$date_from = date('Y-m-d', strtotime('-7 days'));
$date_to = date('Y-m-d');
$date_from = gmdate('Y-m-d', strtotime('-7 days'));
$date_to = gmdate('Y-m-d');
} elseif ($range === '30d') {
$date_from = date('Y-m-d', strtotime('-30 days'));
$date_to = date('Y-m-d');
$date_from = gmdate('Y-m-d', strtotime('-30 days'));
$date_to = gmdate('Y-m-d');
}

// Build query with always using prepare() for safety
Expand Down Expand Up @@ -70,7 +70,7 @@
// Handle CSV export
if (isset($_GET['action']) && $_GET['action'] === 'export_csv' && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'webdecoy_export')) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="webdecoy-detections-' . date('Y-m-d') . '.csv"');
header('Content-Disposition: attachment; filename="webdecoy-detections-' . gmdate('Y-m-d') . '.csv"');

// Neutralize CSV/spreadsheet formula injection: fields like user_agent and
// flags come from untrusted request data, so a value beginning with =, +, -,
Expand Down Expand Up @@ -103,7 +103,7 @@
]);
}

fclose($output);
fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- streaming CSV to php://output for download, not filesystem access
exit;
}

Expand Down
1 change: 1 addition & 0 deletions admin/partials/settings-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ class="regular-text" autocomplete="off" />
<?php if ($last_check) : ?>
<p class="description">
<?php printf(
/* translators: %s: date and time the API connection was last checked */
esc_html__('Last checked: %s', 'webdecoy'),
esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($last_check)))
); ?>
Expand Down
8 changes: 4 additions & 4 deletions admin/partials/statistics-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
$checkout_table = $wpdb->prefix . 'webdecoy_checkout_attempts';

// 30-day detection trend
$thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days'));
$thirty_days_ago = gmdate('Y-m-d H:i:s', strtotime('-30 days'));
$daily_counts = $wpdb->get_results($wpdb->prepare(

Check warning on line 25 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().

Check warning on line 25 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"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",
$thirty_days_ago
), ARRAY_A);
Expand All @@ -30,7 +30,7 @@
// Fill in missing days with 0
$daily_data = [];
for ($i = 29; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$date = gmdate('Y-m-d', strtotime("-{$i} days"));
$daily_data[$date] = 0;
}
foreach ($daily_counts as $row) {
Expand All @@ -40,13 +40,13 @@
}

// Threat level distribution
$threat_distribution = $wpdb->get_results($wpdb->prepare(

Check warning on line 43 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().

Check warning on line 43 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"SELECT threat_level, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY threat_level ORDER BY count DESC",
$thirty_days_ago
), ARRAY_A);

// Top flagged signals (parse from JSON flags)
$recent_flags = $wpdb->get_results($wpdb->prepare(

Check warning on line 49 in admin/partials/statistics-page.php

View workflow job for this annotation

GitHub Actions / PHP Coding Standards

Use of a direct database call is discouraged.
"SELECT flags FROM {$detections_table} WHERE created_at > %s AND flags IS NOT NULL AND flags != '' LIMIT 500",
$thirty_days_ago
), ARRAY_A);
Expand Down Expand Up @@ -92,11 +92,11 @@
$total_30d = array_sum($daily_data);
$total_7d = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s",
date('Y-m-d H:i:s', strtotime('-7 days'))
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",
date('Y-m-d 00:00:00')
gmdate('Y-m-d 00:00:00')
));
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- static query, no user input
$active_blocks = $wpdb->get_var(
Expand Down
12 changes: 7 additions & 5 deletions includes/class-webdecoy-blocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function block(string $ip, string $reason = '', ?int $duration_hours = nu

$expires_at = null;
if ($duration_hours !== null && $duration_hours > 0) {
$expires_at = date('Y-m-d H:i:s', strtotime("+{$duration_hours} hours"));
$expires_at = gmdate('Y-m-d H:i:s', strtotime("+{$duration_hours} hours"));
}

// Check if already blocked
Expand Down Expand Up @@ -273,7 +273,7 @@ public function extend_block(string $ip, int $hours): bool

$new_expires = null;
if ($info['expires_at']) {
$new_expires = date('Y-m-d H:i:s', strtotime($info['expires_at'] . " +{$hours} hours"));
$new_expires = gmdate('Y-m-d H:i:s', strtotime($info['expires_at'] . " +{$hours} hours"));
}

$result = $wpdb->update(
Expand All @@ -300,8 +300,10 @@ public function clear_all(): int

$count = $wpdb->query("DELETE FROM {$table}");

// Clear all cache
wp_cache_flush_group('webdecoy');
// Clear all cache (wp_cache_flush_group() requires WP 6.1+)
if (function_exists('wp_cache_flush_group')) {
wp_cache_flush_group('webdecoy');
}

return $count;
}
Expand Down Expand Up @@ -598,7 +600,7 @@ public function get_stats(): array
// Blocks in last 24 hours
$recent = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE blocked_at > %s",
date('Y-m-d H:i:s', strtotime('-24 hours'))
gmdate('Y-m-d H:i:s', strtotime('-24 hours'))
));

return [
Expand Down
2 changes: 1 addition & 1 deletion includes/class-webdecoy-decoy-response.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private function serve_tarpit(): void
$start = time();
$i = 0;
while ((time() - $start) < $budget) {
echo '<!-- ' . str_repeat('.', 8) . " {$i} -->\n";
echo '<!-- ' . str_repeat('.', 8) . " {$i} -->\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- template-generated decoy content, contains no user input
if (function_exists('flush')) {
@flush(); // phpcs:ignore
}
Expand Down
4 changes: 2 additions & 2 deletions includes/class-webdecoy-detector.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public function get_stats(int $days = 7): array
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_detections';
$since = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days"));

$total = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE created_at > %s",
Expand Down Expand Up @@ -317,7 +317,7 @@ public function get_unique_ips(int $days = 7): int
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_detections';
$since = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days"));

return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(DISTINCT ip_address) FROM {$table} WHERE created_at > %s",
Expand Down
16 changes: 8 additions & 8 deletions includes/class-webdecoy-rate-limiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ public function check_and_increment(string $key): array
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$now = current_time('mysql');
$window_start_threshold = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));
$now = current_time('mysql', true);
$window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));

$existing = $wpdb->get_row($wpdb->prepare(
"SELECT request_count, window_start FROM {$table} WHERE ip_address = %s AND window_start > %s",
Expand Down Expand Up @@ -127,8 +127,8 @@ public function increment(string $ip): int
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$now = current_time('mysql');
$window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));
$now = current_time('mysql', true);
$window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));

// Check for existing record in current window
$existing = $wpdb->get_row($wpdb->prepare(
Expand Down Expand Up @@ -169,7 +169,7 @@ public function get_count(string $ip): int
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));
$window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));

$count = $wpdb->get_var($wpdb->prepare(
"SELECT request_count FROM {$table} WHERE ip_address = %s AND window_start > %s",
Expand Down Expand Up @@ -203,7 +203,7 @@ public function get_reset_time(string $ip): int
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$window_start_threshold = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));
$window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));

$window_start = $wpdb->get_var($wpdb->prepare(
"SELECT window_start FROM {$table} WHERE ip_address = %s AND window_start > %s",
Expand Down Expand Up @@ -246,7 +246,7 @@ public function cleanup(): int
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$threshold = date('Y-m-d H:i:s', strtotime('-1 hour'));
$threshold = gmdate('Y-m-d H:i:s', strtotime('-1 hour'));

return $wpdb->query($wpdb->prepare(
"DELETE FROM {$table} WHERE window_start < %s",
Expand Down Expand Up @@ -339,7 +339,7 @@ public function get_stats(): array
global $wpdb;

$table = $wpdb->prefix . 'webdecoy_rate_limits';
$window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));
$window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds"));

$active_ips = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(DISTINCT ip_address) FROM {$table} WHERE window_start > %s",
Expand Down
6 changes: 3 additions & 3 deletions includes/class-webdecoy-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ function ($order, $request) {
if ($blocker->is_blocked($ip)) {
throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
'webdecoy_blocked',
__('Your checkout has been blocked due to suspicious activity.', 'webdecoy'),
esc_html(__('Your checkout has been blocked due to suspicious activity.', 'webdecoy')),
403
);
}
Expand All @@ -686,7 +686,7 @@ function ($order, $request) {

throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
'webdecoy_velocity',
__('Too many checkout attempts. Please try again later.', 'webdecoy'),
esc_html(__('Too many checkout attempts. Please try again later.', 'webdecoy')),
429
);
}
Expand All @@ -701,7 +701,7 @@ function ($order, $request) {

throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
'webdecoy_carding',
__('Suspicious checkout activity detected.', 'webdecoy'),
esc_html(__('Suspicious checkout activity detected.', 'webdecoy')),
403
);
}
Expand Down
4 changes: 2 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
Contributors: webdecoy
Donate link: https://webdecoy.com
Tags: security, bot detection, spam protection, woocommerce, firewall
Requires at least: 5.6
Tested up to: 6.8
Requires at least: 6.1
Tested up to: 7.0
Stable tag: 2.2.1
Requires PHP: 7.4
License: GPLv2 or later
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/BotDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ public function analyzePath(string $path): array

// Extract path from URL if full URL provided
if (strpos($path, '://') !== false) {
$parsed = parse_url($path);
$parsed = function_exists('wp_parse_url') ? wp_parse_url($path) : parse_url($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- guarded fallback for non-WordPress (standalone SDK) use
$path = ($parsed['path'] ?? '/') . (isset($parsed['query']) ? '?' . $parsed['query'] : '');
}

Expand Down
84 changes: 8 additions & 76 deletions sdk/src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ public function testConnection(): bool
return true;
} catch (WebDecoyException $e) {
if ($e->getCode() === 401 || $e->getCode() === 403) {
throw new WebDecoyException('Invalid API key or insufficient permissions', $e->getCode());
throw new WebDecoyException('Invalid API key or insufficient permissions', $e->getCode()); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser
}
throw $e;
}
Expand All @@ -328,7 +328,7 @@ public function getOrganizationId(): string
/**
* Make an HTTP request to the API
*
* Uses WordPress HTTP API when available, falls back to cURL otherwise.
* Uses the WordPress HTTP API, which is always available inside WordPress.
*
* @param string $method HTTP method
* @param string $endpoint API endpoint
Expand All @@ -349,13 +349,12 @@ private function request(string $method, string $endpoint, array $data = [], boo
$data = [];
}

// Use WordPress HTTP API if available (preferred for WP plugins)
if (function_exists('wp_remote_request')) {
return $this->requestWithWordPress($method, $url, $data, $authenticated);
// Use the WordPress HTTP API (always available inside WordPress).
if (!function_exists('wp_remote_request')) {
throw new WebDecoyException('WordPress HTTP API is unavailable', 0); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser
}

// Fall back to cURL for non-WordPress environments
return $this->requestWithCurl($method, $url, $data, $authenticated);
return $this->requestWithWordPress($method, $url, $data, $authenticated);
}

/**
Expand Down Expand Up @@ -394,7 +393,7 @@ private function requestWithWordPress(string $method, string $url, array $data,
$response = wp_remote_request($url, $args);

if (is_wp_error($response)) {
throw new WebDecoyException('API request failed: ' . $response->get_error_message(), 0);
throw new WebDecoyException('API request failed: ' . $response->get_error_message(), 0); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser
}

$httpCode = wp_remote_retrieve_response_code($response);
Expand All @@ -403,74 +402,7 @@ private function requestWithWordPress(string $method, string $url, array $data,

if ($httpCode >= 400) {
$message = $decoded['error']['message'] ?? $decoded['message'] ?? 'Unknown error';
throw new WebDecoyException("API error: {$message}", $httpCode);
}

return $decoded ?? [];
}

/**
* Make HTTP request using cURL (fallback for non-WordPress environments)
*
* @param string $method HTTP method
* @param string $url Full URL
* @param array $data Request data
* @param bool $authenticated Whether to include API key auth
* @return array Decoded response
* @throws WebDecoyException On error
*/
private function requestWithCurl(string $method, string $url, array $data, bool $authenticated): array
{
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: ' . self::userAgent(),
];

if ($authenticated) {
$headers[] = 'Authorization: Bearer ' . $this->apiKey;
}

$ch = curl_init();

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
]);

switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
}

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);

curl_close($ch);

if ($error) {
throw new WebDecoyException("API request failed: {$error}", 0);
}

$decoded = json_decode($response, true);

if ($httpCode >= 400) {
$message = $decoded['error']['message'] ?? $decoded['message'] ?? 'Unknown error';
throw new WebDecoyException("API error: {$message}", $httpCode);
throw new WebDecoyException("API error: {$message}", $httpCode); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser
}

return $decoded ?? [];
Expand Down
8 changes: 7 additions & 1 deletion sdk/src/GoodBotList.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace WebDecoy;

if (!defined('ABSPATH')) {
exit;
}

// PHP 7.4 polyfill for str_ends_with (available in PHP 8.0+)
// Kept here for non-WordPress contexts where webdecoy.php is not loaded
if (!function_exists('str_ends_with')) {
Expand Down Expand Up @@ -692,7 +696,9 @@ private function performReverseDNSVerification(string $ip, array $expectedSuffix
// Step 2: Check if hostname ends with expected suffix
$matchesSuffix = false;
foreach ($expectedSuffixes as $suffix) {
if (str_ends_with($hostname, strtolower($suffix))) {
$suffixLower = strtolower($suffix);
$suffixLen = strlen($suffixLower);
if ($suffixLen === 0 || substr($hostname, -$suffixLen) === $suffixLower) {
$matchesSuffix = true;
break;
}
Expand Down
Loading
Loading