PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/component/class-notfound-lockout.php +134 -115 5.6.1 → 6.3.0 View file →
@@ -10,8 +10,9 @@
10 10 use WP_Defender\Component;
11 11 use WP_Defender\Traits\Country;
12 12 use WP_Defender\Model\Lockout_Ip;
13 13 use WP_Defender\Model\Lockout_Log;
14 +use WP_Defender\Component\Known_Bots\Known_Bots_Factory;
14 15
15 16 /**
16 17 * Handles the detection of 404 errors and manages lockouts based on configured settings.
17 18 */
@@ -19,8 +20,15 @@
19 20
20 21 use Country;
21 22
22 23 public const SCENARIO_ERROR_404 = 'error_404', SCENARIO_ERROR_404_IGNORE = 'error_404_ignore', SCENARIO_LOCKOUT_404 = '404_lockout';
24 + // New log types for 404_INTELLIGENCE.
25 + public const SCENARIO_404_INTELLIGENCE_XSS_ATTEMPT = '404_xss_attempt';
26 + public const SCENARIO_404_INTELLIGENCE_XSS_LOCKOUT = '404_xss_lockout';
27 + public const SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_ATTEMPT = '404_non_existent_plugin_attempt';
28 + public const SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_LOCKOUT = '404_non_existent_plugin_lockout';
29 + public const SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_ATTEMPT = '404_non_existent_theme_attempt';
30 + public const SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_LOCKOUT = '404_non_existent_theme_lockout';
23 31 /**
24 32 * Use for cache.
25 33 *
26 34 * @var \WP_Defender\Model\Setting\Notfound_Lockout
@@ -27,134 +35,122 @@
27 35 */
28 36 public $model;
29 37
30 38 /**
39 + * Mark to separate standard logs from intelligence ones and avoid duplicate.
40 + *
41 + * @var bool
42 + */
43 + private $is_intelligence = false;
44 +
45 + /**
31 46 * Constructor for Notfound_Lockout.
32 47 */
33 48 public function __construct() {
34 49 $this->model = wd_di()->get( \WP_Defender\Model\Setting\Notfound_Lockout::class );
50 + add_action( 'init', array( $this, 'maybe_detect_suspicious_request' ), 1 );
35 51 }
36 -
37 52 /**
38 - * Queue hooks when this class init.
53 + * Run 404 intelligence.
39 54 */
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 );
55 + public function maybe_detect_suspicious_request() {
56 + // Don't interfere with WP-CLI, Cron, AJAX and legit internal requests.
57 + if ( defender_is_wp_cli() ) {
58 + return;
58 59 }
59 - if ( function_exists( 'mb_strtolower' ) ) {
60 - $user_agent = mb_strtolower( $user_agent, 'UTF-8' );
61 - } else {
62 - $user_agent = strtolower( $user_agent );
60 + if ( wp_doing_cron() ) {
61 + return;
63 62 }
63 + if ( wp_doing_ajax() ) {
64 + return;
65 + }
66 + if ( false === $this->model->detect_logged && is_user_logged_in() ) {
67 + return;
68 + }
64 69
65 - if ( false !== stristr( $user_agent, 'googlebot' ) ) {
66 - return true;
70 + $uri = defender_get_data_from_request( 'REQUEST_URI', 's' );
71 + $uri = urldecode( $uri );
72 + $query = defender_get_data_from_request( 'QUERY_STRING', 's' );
73 +
74 + $service = wd_di()->get( Blacklist_Lockout::class );
75 + foreach ( $this->get_user_ip() as $ip ) {
76 + if ( ! $service->is_ip_whitelisted( $ip ) ) {
77 + $this->detect_suspicious_request( $ip, $uri, $query );
78 + }
67 79 }
68 -
69 - return false;
70 80 }
71 81
72 82 /**
73 - * Check if IP is from Google, base on https://support.google.com/webmasters/answer/80553?hl=en.
83 + * Detect if the current request:
84 + * -refers to a non-existent plugin or theme,
85 + * -contains XSS attempts.
74 86 *
75 - * @param string $ip The IP address to check.
87 + * @param string $ip The IP address to process.
88 + * @param string $uri The URI that was accessed.
89 + * @param string $query The query that was accessed.
76 90 *
77 - * @return bool
91 + * @return void
78 92 */
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 );
93 + protected function detect_suspicious_request( $ip, $uri, $query ) {
94 + $lockout_type = '';
84 95
85 - if ( ! is_array( $hosts ) ) {
86 - return false;
87 - }
96 + $xss_pattern = '#(?i)(<script|javascript:|onerror=|onload=|onmouseover=|onclick=|alert\s*\(|prompt\s*\(|confirm\s*\(|document\.cookie|document\.write|eval\s*\()#';
97 + if ( preg_match( $xss_pattern, $uri ) || preg_match( $xss_pattern, $query ) ) {
98 + $slug = $uri;
99 + $attempt_type = self::SCENARIO_404_INTELLIGENCE_XSS_ATTEMPT;
100 + $lockout_type = self::SCENARIO_404_INTELLIGENCE_XSS_LOCKOUT;
101 + } elseif ( preg_match( '#/wp-content/(plugins|themes)/([^/]+)/#i', $uri, $m ) ) {
102 + // Looking for requests for plugins/themes.
103 + $entity = strtolower( $m[1] );
104 + $slug = sanitize_file_name( $m[2] );
105 + $path = ( 'themes' === $entity ? get_theme_root() : WP_PLUGIN_DIR ) . '/' . $slug;
88 106
89 - // Check if this match the original ip.
90 - foreach ( $hosts as $host ) {
91 - if ( $ip === $host ) {
92 - return true;
107 + // Check for the existence of the specified file path. If it doesn't exist, only then continue working on the request.
108 + if ( ! file_exists( $path ) ) {
109 + if ( 'themes' === $entity ) {
110 + $attempt_type = self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_ATTEMPT;
111 + $lockout_type = self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_LOCKOUT;
112 + } else {
113 + $attempt_type = self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_ATTEMPT;
114 + $lockout_type = self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_LOCKOUT;
93 115 }
94 116 }
95 117 }
96 118
97 - return false;
98 - }
119 + if ( '' !== $lockout_type ) {
120 + // Detect it.
121 + $model = Lockout_Ip::get( $ip );
122 + $this->log_event( $ip, $slug, $attempt_type );
99 123
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 {
124 + // Avoid the duplicate with the standard 404 case.
125 + $this->is_intelligence = true;
108 126
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 - }
127 + $model = $this->record_fail_attempt( $ip, $model );
128 + // Count the attempt.
129 + $window = strtotime( '- ' . $this->model->timeframe . ' seconds' );
116 130
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';
131 + $model = $this->check_meta_data( $model );
132 + // We will get the latest till oldest, limit by attempt.
133 + $checks = array_slice( $model->meta['nf'], $this->model->attempt * - 1 );
124 134
125 - if ( preg_match( '/' . $msn_ua . '/i', $user_agent ) ) {
126 - return true;
135 + if ( count( $checks ) < $this->model->attempt ) {
136 + return;
137 + }
138 + // If the last time is larger.
139 + $check = min( $checks );
140 + if ( $check >= $window ) {
141 + // then lock it.
142 + $this->lock( $model, 'normal', $uri );
143 + $this->log_event( $ip, $uri, $lockout_type );
144 + }
127 145 }
128 -
129 - return false;
130 146 }
131 147
132 148 /**
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
149 + * Queue hooks when this class init.
138 150 */
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;
151 + public function add_hooks() {
152 + add_action( 'template_redirect', array( $this, 'process_404_detect_multiple' ) );
157 153 }
158 154
159 155 /**
160 156 * Processes 404 detection for a single IP address.
@@ -161,16 +157,12 @@
161 157 *
162 158 * @param string $ip The IP address to process.
163 159 */
164 160 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 ) ) {
161 + // Check if request is from a legitimate known bot (Google, Bing, Facebook).
162 + if ( Known_Bots_Factory::create()->is_known_bot( $ip ) ) {
167 163 return;
168 164 }
169 - // or bing.
170 - if ( $this->is_bing_ua() && $this->is_bing_ip( $ip ) ) {
171 - return;
172 - }
173 165
174 166 $uri = defender_get_data_from_request( 'REQUEST_URI', 's' );
175 167 // Strip encode.
176 168 $uri = urldecode( $uri );
@@ -332,22 +324,10 @@
332 324 * @param string $uri The URI that was accessed.
333 325 * @param string $scenario The scenario under which the event is logged.
334 326 */
335 327 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();
328 + $model = Lockout_Log::create( $ip, $uri );
343 329
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 330 switch ( $scenario ) {
351 331 case self::SCENARIO_ERROR_404:
352 332 $model->type = Lockout_Log::ERROR_404;
353 333 $model->log = sprintf(
@@ -363,8 +343,47 @@
363 343 esc_html__( 'Request for file %s which doesn`t exist', 'defender-security' ),
364 344 $uri
365 345 );
366 346 break;
347 + // New log cases since v5.7.0.
348 + case self::SCENARIO_404_INTELLIGENCE_XSS_ATTEMPT:
349 + $model->type = Lockout_Log::ERROR_404_XSS;
350 + $model->log = sprintf(
351 + /* translators: %s: URI. */
352 + esc_html__( 'Possible XSS attempt detected in request: %s', 'defender-security' ),
353 + $uri
354 + );
355 + break;
356 + case self::SCENARIO_404_INTELLIGENCE_XSS_LOCKOUT:
357 + $model->type = Lockout_Log::LOCKOUT_404_XSS;
358 + $model->log = esc_html__( 'Lockout occurred: Multiple XSS attempt requests detected', 'defender-security' );
359 + break;
360 + case self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_ATTEMPT:
361 + $model->type = Lockout_Log::ERROR_404_NON_EXISTENT_PLUGIN;
362 + $model->log = sprintf(
363 + /* translators: %s: URI. */
364 + esc_html__( 'Attempt to access non-installed plugin: %s', 'defender-security' ),
365 + $uri
366 + );
367 + break;
368 + case self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_PLUGIN_LOCKOUT:
369 + $model->type = Lockout_Log::LOCKOUT_404_NON_EXISTENT_PLUGIN;
370 + $model->log =
371 + esc_html__( 'Lockout occurred: Multiple requests for non-installed plugin', 'defender-security' );
372 + break;
373 + case self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_ATTEMPT:
374 + $model->type = Lockout_Log::ERROR_404_NON_EXISTENT_THEME;
375 + $model->log = sprintf(
376 + /* translators: %s: URI. */
377 + esc_html__( 'Attempt to access non-installed theme: %s', 'defender-security' ),
378 + $uri
379 + );
380 + break;
381 + case self::SCENARIO_404_INTELLIGENCE_NON_EXISTENT_THEME_LOCKOUT:
382 + $model->type = Lockout_Log::LOCKOUT_404_NON_EXISTENT_THEME;
383 + $model->log =
384 + esc_html__( 'Lockout occurred: Multiple requests for non-installed theme', 'defender-security' );
385 + break;
367 386 case self::SCENARIO_LOCKOUT_404:
368 387 default:
369 388 $model->type = Lockout_Log::LOCKOUT_404;
370 389 $model->log = sprintf(
@@ -377,9 +396,9 @@
377 396 $model->save();
378 397 // @since 5.4.0 Action hook triggered after a failed 404 attempt.
379 398 do_action( 'wd_404_attempt', $model, $scenario, $uri );
380 399
381 - if ( Lockout_Log::LOCKOUT_404 === $model->type ) {
400 + if ( in_array( $model->type, Lockout_Log::get_404_lockout_types(), true ) ) {
382 401 do_action( 'defender_notify', 'firewall-notification', $model );
383 402 }
384 403 }
385 404
@@ -389,9 +408,9 @@
389 408 * @return void
390 409 * @since 4.4.2
391 410 */
392 411 public function process_404_detect_multiple(): void {
393 - if ( ! is_404() ) {
412 + if ( $this->is_intelligence || ! is_404() ) {
394 413 return;
395 414 }
396 415
397 416 if ( false === $this->model->detect_logged && is_user_logged_in() ) {