PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.1
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-notfound-lockout.php

class-notfound-lockout.php in Defender Security – Malware Scanner, Login Security & Firewall 5.3.1, at src/component/class-notfound-lockout.php

406 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling 404 error detections and lockouts.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Component;
11 use WP_Defender\Traits\Country;
12 use WP_Defender\Model\Lockout_Ip;
13 use WP_Defender\Model\Lockout_Log;
14
15 /**
16 * Handles the detection of 404 errors and manages lockouts based on configured settings.
17 */
18 class Notfound_Lockout extends Component {
19
20 use Country;
21
22 public const SCENARIO_ERROR_404 = 'error_404', SCENARIO_ERROR_404_IGNORE = 'error_404_ignore', SCENARIO_LOCKOUT_404 = '404_lockout';
23 /**
24 * Use for cache.
25 *
26 * @var \WP_Defender\Model\Setting\Notfound_Lockout
27 */
28 public $model;
29
30 /**
31 * Constructor for Notfound_Lockout.
32 */
33 public function __construct() {
34 $this->model = wd_di()->get( \WP_Defender\Model\Setting\Notfound_Lockout::class );
35 }
36
37 /**
38 * Queue hooks when this class init.
39 */
40 public function add_hooks() {
41 add_action( 'template_redirect', array( $this, 'process_404_detect_multiple' ) );
42 }
43
44 /**
45 * Check if useragent is looks like from googlebot.
46 *
47 * @param string $user_agent The user agent string to check.
48 *
49 * @return bool
50 */
51 private function is_google_ua( $user_agent = '' ): bool {
52 if ( empty( $user_agent ) ) {
53 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
54 if ( empty( $user_agent ) ) {
55 return false;
56 }
57 $user_agent = User_Agent::fast_cleaning( $user_agent );
58 }
59 if ( function_exists( 'mb_strtolower' ) ) {
60 $user_agent = mb_strtolower( $user_agent, 'UTF-8' );
61 } else {
62 $user_agent = strtolower( $user_agent );
63 }
64
65 if ( false !== stristr( $user_agent, 'googlebot' ) ) {
66 return true;
67 }
68
69 return false;
70 }
71
72 /**
73 * Check if IP is from Google, base on https://support.google.com/webmasters/answer/80553?hl=en.
74 *
75 * @param string $ip The IP address to check.
76 *
77 * @return bool
78 */
79 private function is_google_ip( $ip ): bool {
80 $hostname = gethostbyaddr( $ip );
81 // Check if this hostname has googlebot or google.com.
82 if ( preg_match( '/\.googlebot|google\.com$/i', $hostname ) ) {
83 $hosts = gethostbynamel( $hostname );
84
85 if ( ! is_array( $hosts ) ) {
86 return false;
87 }
88
89 // Check if this match the original ip.
90 foreach ( $hosts as $host ) {
91 if ( $ip === $host ) {
92 return true;
93 }
94 }
95 }
96
97 return false;
98 }
99
100 /**
101 * Checks if the user agent belongs to Bing.
102 *
103 * @param string $user_agent The user agent string to check.
104 *
105 * @return bool Returns true if the user agent is identified as belonging to Bing, false otherwise.
106 */
107 private function is_bing_ua( $user_agent = '' ): bool {
108
109 if ( empty( $user_agent ) ) {
110 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
111 if ( empty( $user_agent ) ) {
112 return false;
113 }
114 $user_agent = User_Agent::fast_cleaning( $user_agent );
115 }
116
117 if ( function_exists( 'mb_strtolower' ) ) {
118 $user_agent = mb_strtolower( $user_agent, 'UTF-8' );
119 } else {
120 $user_agent = strtolower( $user_agent );
121 }
122 // MSN Bot Useragent https://www.bing.com/webmaster/help/which-crawlers-does-bing-use-8c184ec0.
123 $msn_ua = 'Bingbot|MSNBot|MSNBot-Media|AdIdxBot|BingPreview';
124
125 if ( preg_match( '/' . $msn_ua . '/i', $user_agent ) ) {
126 return true;
127 }
128
129 return false;
130 }
131
132 /**
133 * Check if IP is from Bing, base on https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26.
134 *
135 * @param string $ip The IP address to check.
136 *
137 * @return bool
138 */
139 private function is_bing_ip( $ip ): bool {
140 $hostname = gethostbyaddr( $ip );
141 if ( preg_match( '/\.msnbot|msn\.com$/i', $hostname ) ) {
142 $hosts = gethostbynamel( $hostname );
143
144 if ( ! is_array( $hosts ) ) {
145 return false;
146 }
147
148 // Check if this match the original ip.
149 foreach ( $hosts as $host ) {
150 if ( $ip === $host ) {
151 return true;
152 }
153 }
154 }
155
156 return false;
157 }
158
159 /**
160 * Processes 404 detection for a single IP address.
161 *
162 * @param string $ip The IP address to process.
163 */
164 public function process_404_detect( string $ip ): void {
165 // Check if this from Google.
166 if ( $this->is_google_ua() && $this->is_google_ip( $ip ) ) {
167 return;
168 }
169 // or bing.
170 if ( $this->is_bing_ua() && $this->is_bing_ip( $ip ) ) {
171 return;
172 }
173
174 $uri = defender_get_data_from_request( 'REQUEST_URI', 's' );
175 // Strip encode.
176 $uri = urldecode( $uri );
177 $model = Lockout_Ip::get( $ip );
178 $model = $this->record_fail_attempt( $ip, $model );
179
180 $ext = pathinfo( $uri, PATHINFO_EXTENSION );
181 $ext = trim( $ext );
182 // Downfall from match URL to extension.
183 foreach ( $this->model->get_lockout_list( 'allowlist' ) as $pattern ) {
184 $pattern = preg_quote( $pattern, '/' );
185 if ( preg_match( '/' . $pattern . '$/i', $uri ) ) {
186 // Whitelisted, just return.
187 return;
188 }
189 }
190
191 foreach ( $this->model->get_lockout_list( 'blocklist' ) as $pattern ) {
192 $pattern = preg_quote( $pattern, '/' );
193 if ( preg_match( '/' . $pattern . '$/i', $uri ) ) {
194 $this->lock( $model, 'blacklist', $uri );
195 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
196
197 return;
198 }
199 }
200
201 if ( strlen( $ext ) ) {
202 // If ext isn't null.
203 foreach ( $this->model->get_lockout_list( 'allowlist' ) as $whitelist_ext ) {
204 if ( str_replace( '.', '', strtolower( $whitelist_ext ) ) === $ext ) {
205 // Ext is whitelist, log and return.
206 $this->log_event( $ip, $uri, self::SCENARIO_ERROR_404_IGNORE );
207
208 return;
209 }
210 }
211
212 foreach ( $this->model->get_lockout_list( 'blocklist' ) as $blacklist_ext ) {
213 if ( str_replace( '.', '', strtolower( $blacklist_ext ) ) === $ext ) {
214 // Block it.
215 $this->lock( $model, 'blacklist', $uri );
216 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
217
218 return;
219 }
220 }
221 }
222
223 $this->log_event( $ip, $uri, self::SCENARIO_ERROR_404 );
224
225 // Count the attempt.
226 $window = strtotime( '- ' . $this->model->timeframe . ' seconds' );
227
228 $model = $this->check_meta_data( $model );
229 // We will get the latest till oldest, limit by attempt.
230 $checks = array_slice( $model->meta['nf'], $this->model->attempt * - 1 );
231
232 if ( count( $checks ) < $this->model->attempt ) {
233 return;
234 }
235 // If the last time is larger.
236 $check = min( $checks );
237 if ( $check >= $window ) {
238 // then lock it.
239 $this->lock( $model, 'normal', $uri );
240 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
241 }
242 }
243
244 /**
245 * Locks the IP and updates the model accordingly.
246 *
247 * @param Lockout_Ip $model The Lockout_Ip model instance.
248 * @param string $scenario The scenario under which the lock is being applied.
249 * @param string $uri The URI that triggered the lockout.
250 */
251 private function lock( Lockout_Ip $model, $scenario = 'normal', $uri = '' ) {
252 if ( 'permanent' === $this->model->lockout_type ) {
253 $scenario = 'blacklist';
254 }
255
256 if ( 'blacklist' === $scenario ) {
257 $model->attempt_404 = 0;
258 $model->meta['nf'] = array();
259 } else {
260 $model->status = Lockout_Ip::STATUS_BLOCKED;
261 // @since 3.7.0. The lock_time column is used for temporary lockouts.
262 $model->lock_time = time();
263 $model->release_time = strtotime( '+ ' . $this->model->duration . ' ' . $this->model->duration_unit );
264 $model->lockout_message = $this->model->lockout_message;
265 }
266
267 $model->save();
268
269 if ( 'blacklist' === $scenario ) {
270 do_action( 'wd_blacklist_this_ip', $model->ip );
271 }
272
273 /**
274 * Action hook triggered when a user is locked due to a 404 lockout.
275 *
276 * @param Lockout_Ip $model The Lockout_Ip object representing the IP address being locked.
277 * @param string $scenario The scenario of the IP lockout ("normal" or "blacklist").
278 * @param string $uri The URI associated with the 404 lockout.
279 *
280 * @since 4.3.0 The `$uri` parameter was added.
281 */
282 do_action( 'wd_404_lockout', $model, $scenario, $uri );
283 }
284
285 /**
286 * Store the fail attempt of current IP.
287 *
288 * @param string $ip The IP address.
289 * @param Lockout_Ip $model The Lockout_Ip model instance.
290 *
291 * @return Lockout_Ip
292 */
293 protected function record_fail_attempt( $ip, $model ): Lockout_Ip {
294 // Fix warning with a non-numeric value.
295 if ( ! is_numeric( $model->attempt_404 ) ) {
296 $model->attempt_404 = 1;
297 } else {
298 ++$model->attempt_404;
299 }
300 $model->ip = $ip;
301
302 $model = $this->check_meta_data( $model );
303 // Cache the time here, so it consumes less memory than query the logs.
304 $model->meta['nf'][] = time();
305 $model->save();
306
307 return $model;
308 }
309
310 /**
311 * Ensures the metadata for the model is correctly initialized and formatted.
312 *
313 * @param Lockout_Ip $model The model to check and update.
314 *
315 * @return Lockout_Ip The checked and potentially modified model.
316 */
317 protected function check_meta_data( &$model ): Lockout_Ip {
318 if (
319 ! isset( $model->meta['nf'] ) ||
320 ( isset( $model->meta['nf'] ) && ! is_array( $model->meta['nf'] ) )
321 ) {
322 $model->meta['nf'] = array();
323 }
324
325 return $model;
326 }
327
328 /**
329 * Log the event into db, we will use the data in logs page later.
330 *
331 * @param string $ip The IP address involved in the event.
332 * @param string $uri The URI that was accessed.
333 * @param string $scenario The scenario under which the event is logged.
334 */
335 public function log_event( $ip, $uri, $scenario ) {
336 $model = new Lockout_Log();
337 $model->ip = $ip;
338 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
339 $model->user_agent = isset( $user_agent ) ? User_Agent::fast_cleaning( $user_agent ) : null;
340 $model->date = time();
341 $model->tried = $uri;
342 $model->blog_id = get_current_blog_id();
343
344 $ip_to_country = $this->ip_to_country( $ip );
345
346 if ( isset( $ip_to_country['iso'] ) ) {
347 $model->country_iso_code = $ip_to_country['iso'];
348 }
349
350 switch ( $scenario ) {
351 case self::SCENARIO_ERROR_404:
352 $model->type = Lockout_Log::ERROR_404;
353 $model->log = sprintf(
354 /* translators: %s: URI. */
355 esc_html__( 'Request for file %s which doesn`t exist', 'defender-security' ),
356 $uri
357 );
358 break;
359 case self::SCENARIO_ERROR_404_IGNORE:
360 $model->type = Lockout_Log::ERROR_404_IGNORE;
361 $model->log = sprintf(
362 /* translators: %s: URI. */
363 esc_html__( 'Request for file %s which doesn`t exist', 'defender-security' ),
364 $uri
365 );
366 break;
367 case self::SCENARIO_LOCKOUT_404:
368 default:
369 $model->type = Lockout_Log::LOCKOUT_404;
370 $model->log = sprintf(
371 /* translators: %s: URI. */
372 esc_html__( 'Lockout occurred: Too many 404 requests for %s', 'defender-security' ),
373 $uri
374 );
375 break;
376 }
377 $model->save();
378 if ( Lockout_Log::LOCKOUT_404 === $model->type ) {
379 do_action( 'defender_notify', 'firewall-notification', $model );
380 }
381 }
382
383 /**
384 * Process 404 detection for multiple IPs.
385 *
386 * @return void
387 * @since 4.4.2
388 */
389 public function process_404_detect_multiple(): void {
390 if ( ! is_404() ) {
391 return;
392 }
393
394 if ( false === $this->model->detect_logged && is_user_logged_in() ) {
395 return;
396 }
397
398 $service = wd_di()->get( Blacklist_Lockout::class );
399 foreach ( $this->get_user_ip() as $ip ) {
400 if ( ! $service->is_ip_whitelisted( $ip ) ) {
401 $this->process_404_detect( $ip );
402 }
403 }
404 }
405 }
406