PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.0
Defender Security – Malware Scanner, Login Security & Firewall v5.6.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-fake-bot-detection.php

class-fake-bot-detection.php in Defender Security – Malware Scanner, Login Security & Firewall 5.6.0, at src/component/class-fake-bot-detection.php

306 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles fake bot detection functionality.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Component;
11 use WP_Defender\Controller\Firewall;
12 use WP_Defender\Model\Lockout_Log;
13 use WP_Defender\Model\Setting\User_Agent_Lockout;
14 use WP_Defender\Model\Lockout_Ip;
15 use WP_Defender\Traits\Country;
16
17 /**
18 * Class Fake_Bot_Detection
19 *
20 * Detects whether the current HTTP request comes from a legitimate search engine
21 * or monitoring crawler (Googlebot, Bingbot, Yandex, etc.).
22 */
23 class Fake_Bot_Detection extends Component {
24 use Country;
25
26 public const SCENARIO_FAKE_BOT = 'fake_bot';
27
28 /**
29 * Remote JSON URL containing crawler definitions.
30 *
31 * @var string
32 */
33 public const REMOTE_URL = 'https://gist.githubusercontent.com/incsub/8ab5815980fc8af44fe0223f8dcddb66/raw/3ddcc5be3b007afc8cd4f4af4b45f36d5c60508e/crawlers.json';
34
35 /**
36 * Local fallback path to crawler definitions JSON.
37 *
38 * @var string
39 */
40 public const LOCAL_FILE = 'includes/crawlers.json';
41
42 /**
43 * WP transient key for caching crawler data.
44 */
45 public const CACHE_KEY = 'wpmu_crawlers';
46
47 /**
48 * Cache expiration (12 hours).
49 */
50 public const CACHE_TTL = 12 * HOUR_IN_SECONDS;
51
52 /**
53 * Array of crawler definitions loaded from JSON.
54 *
55 * Format example:
56 * [
57 * "Google" => [
58 * "ips" => ["ipv4" => [], "ipv6" => []],
59 * "hostname_pattern" => "googlebot.com",
60 * "user_agents" => ["Googlebot"]
61 * ],
62 * ...
63 * ]
64 *
65 * @var array<string, array>
66 */
67 protected array $crawlers = array();
68
69 /**
70 * The model for handling the data.
71 *
72 * @var User_Agent_Lockout
73 */
74 protected $model;
75
76 /**
77 * Fake_Bot_Detection constructor.
78 *
79 * @param User_Agent_Lockout $model The model instance for fake bot detection functionality.
80 */
81 public function __construct( User_Agent_Lockout $model ) {
82 $this->model = $model;
83 }
84
85 /**
86 * Check if the fake bot detection is enabled.
87 */
88 public function is_enabled(): bool {
89 return $this->model->enabled && $this->model->fake_bots_enabled;
90 }
91
92 /**
93 * Determine if the current HTTP request is from a legitimate crawler.
94 *
95 * The detection flow is:
96 * 1. Check User-Agent against known crawler UAs.
97 * 2. If UA matches, check the request IP against the crawler's allowlist.
98 * 3. If not in allowlist, perform reverse DNS lookup (IP → hostname).
99 * 4. Verify that the hostname matches known crawler domain patterns.
100 * 5. Perform forward DNS lookup (hostname → IP) and ensure the original IP matches.
101 *
102 * Crawler definitions (UA, IP ranges, hostname patterns) are loaded from
103 * a remote JSON file (e.g. GitHub Gist). If that fails, a local fallback
104 * JSON file bundled with the plugin is used.
105 *
106 * @return void
107 */
108 public function validate_legit_crawler(): void {
109 $ips = $this->get_user_ip();
110 $agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
111
112 if ( array() === $ips || '' === $agent ) {
113 return;
114 }
115
116 foreach ( $this->crawlers as $name => $data ) {
117 // 1. User-Agent check.
118 if ( ! $this->match_user_agent( $agent, $data['user_agents'] ?? array() ) ) {
119 continue;
120 }
121
122 // Check each IP against the allowed list.
123 foreach ( $ips as $ip ) {
124 // 2. IP allowlist check.
125 $allowed_ips = array_merge(
126 $data['ips']['ipv4'] ?? array(),
127 $data['ips']['ipv6'] ?? array()
128 );
129
130 if ( array() !== $allowed_ips && $this->is_ip_in_format( $ip, $allowed_ips ) ) {
131 return;
132 }
133
134 // 3. Reverse DNS lookup.
135 $hostname = gethostbyaddr( $ip );
136 if ( ! $hostname || $hostname === $ip ) {
137 $this->block_ip( $ip, $agent, $name );
138 return;
139 }
140
141 $pattern = $data['hostname_pattern'] ?? '';
142 if ( '' === $pattern || ! preg_match( '#' . $pattern . '#i', $hostname ) ) {
143 $this->block_ip( $ip, $agent, $name );
144 return; // Hostname doesn't match.
145 }
146
147 // 4. Forward DNS lookup
148 $resolved_ips = gethostbynamel( $hostname );
149 if ( is_array( $resolved_ips ) && in_array( $ip, $resolved_ips, true ) ) {
150 return; // Verified by forward + reverse DNS.
151 }
152 }
153
154 $this->block_ip( $ip, $agent, $name );
155 return; // UA matched, but DNS validation failed.
156 }
157 }
158
159 /**
160 * Load crawler definitions from remote URL with local fallback.
161 *
162 * @return void
163 */
164 public function load_crawlers(): void {
165 // 1. Try cache first.
166 $cached = get_site_transient( self::CACHE_KEY );
167 if ( is_array( $cached ) ) {
168 $this->crawlers = $cached;
169 return;
170 }
171
172 // 2. Attempt remote fetch.
173 $response = wp_remote_get( self::REMOTE_URL, array( 'timeout' => 5 ) );
174
175 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
176 $json = wp_remote_retrieve_body( $response );
177 $data = json_decode( $json, true );
178
179 if ( is_array( $data ) ) {
180 $this->crawlers = $data;
181
182 // Cache remote result for 12h.
183 set_site_transient( self::CACHE_KEY, $this->crawlers, self::CACHE_TTL );
184 return;
185 }
186 }
187
188 // 3. Remote failed → fallback to local file (not cached).
189 $local_path = defender_path( self::LOCAL_FILE );
190 if ( file_exists( $local_path ) ) {
191 $json = file_get_contents( $local_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
192 $data = json_decode( $json, true );
193 $this->crawlers = is_array( $data ) ? $data : array();
194 } else {
195 $this->crawlers = array();
196 }
197 }
198
199
200 /**
201 * Test User-Agent string against known crawler substrings.
202 *
203 * @param string $agent Full User-Agent string from request.
204 * @param array $patterns List of substrings to match against.
205 *
206 * @return bool True if UA contains one of the patterns, false otherwise.
207 */
208 protected function match_user_agent( string $agent, array $patterns ): bool {
209 foreach ( $patterns as $ua ) {
210 if ( stripos( $agent, $ua ) !== false ) {
211 return true;
212 }
213 }
214 return false;
215 }
216
217 /**
218 * Block the given IP address according to settings.
219 *
220 * @param string $ip The IP address to block.
221 * @param string $user_agent The User-Agent string from the request.
222 * @param string $bot_name The User-Agent label impersonated (e.g. "Googlebot").
223 */
224 protected function block_ip( string $ip, string $user_agent, string $bot_name ): void {
225 $lockout_model = Lockout_Ip::get( $ip );
226 $remaining_time = 0;
227 if ( 'permanent' === $this->model->fake_bots_lockout_type ) {
228 $lockout_model->attempt = 0;
229 $lockout_model->meta['login'] = array();
230 $lockout_model->meta['nf'] = array();
231 $lockout_model->save();
232 // We block IP here unlike other UA lockout cases.
233 do_action( 'wd_blacklist_this_ip', $ip );
234 } else {
235 $lockout_model->status = Lockout_Ip::STATUS_BLOCKED;
236 $lockout_model->lock_time = time();
237
238 $this->create_blocked_lockout(
239 $lockout_model,
240 $this->model->fake_bots_message,
241 strtotime( '+' . $this->model->fake_bots_lockout_duration . ' ' . $this->model->fake_bots_lockout_duration_unit )
242 );
243
244 $remaining_time = $lockout_model->remaining_release_time();
245 }
246
247 $this->log_event( $ip, self::SCENARIO_FAKE_BOT, $bot_name );
248
249 wd_di()->get( Firewall::class )->actions_for_blocked(
250 $this->model->fake_bots_message,
251 $remaining_time,
252 self::SCENARIO_FAKE_BOT,
253 $this->get_user_ip()
254 );
255 }
256
257 /**
258 * Log the event into db, we will use the data in logs page later.
259 *
260 * @param string $ip The IP address involved in the event.
261 * @param string $scenario The scenario under which the event is logged.
262 * @param string $bot_name The name of the bot being impersonated.
263 */
264 public function log_event( $ip, $scenario, $bot_name ) {
265 $model = new Lockout_Log();
266 $model->ip = $ip;
267 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
268 $model->user_agent = isset( $user_agent ) ? User_Agent::fast_cleaning( $user_agent ) : null;
269 $model->date = time();
270 $model->tried = $user_agent;
271 $model->blog_id = get_current_blog_id();
272
273 $ip_to_country = $this->ip_to_country( $ip );
274
275 if ( ! empty( $ip_to_country ) && isset( $ip_to_country['iso'] ) ) {
276 $model->country_iso_code = $ip_to_country['iso'];
277 }
278
279 switch ( $scenario ) {
280 case self::SCENARIO_FAKE_BOT:
281 default:
282 $model->type = Lockout_Log::LOCKOUT_FAKE_BOT;
283 $model->log = sprintf(
284 /* translators: %s: The name of the bot being impersonated. */
285 esc_html__( 'Lockout occurred: Fake bot impersonated %s.', 'defender-security' ),
286 $bot_name
287 );
288 break;
289 }
290 $model->save();
291 }
292
293 /**
294 * Creates a lockout for a blocked IP.
295 *
296 * @param Lockout_Ip $model The lockout IP model.
297 * @param string $message The lockout message.
298 * @param int $time The timestamp when the lockout will be lifted.
299 */
300 protected function create_blocked_lockout( &$model, $message, $time ) {
301 $model->lockout_message = $message;
302 $model->release_time = $time;
303 $model->save();
304 }
305 }
306