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-user-agent.php +119 -34 5.3.1 → 6.3.0 View file →
@@ -12,8 +12,9 @@
12 12 use WP_Defender\Model\Lockout_Ip;
13 13 use WP_Defender\Model\Lockout_Log;
14 14 use WP_Defender\Model\Setting\User_Agent_Lockout;
15 15 use WP_Defender\Model\Notification\Firewall_Notification;
16 +use WP_Filesystem_Base;
16 17
17 18 /**
18 19 * Handles User-Agent based operations including lockouts and logging for security purposes.
19 20 *
@@ -31,8 +32,11 @@
31 32 * Human Readable text denotes user agent header is empty.
32 33 */
33 34 public const EMPTY_USER_AGENT_TEXT = 'Empty User Agent';
34 35
36 + public const GO_HTTP_CLIENT_KEY = 'go-http-client';
37 + public const PYTHON_REQUESTS_KEY = 'python-requests';
38 +
35 39 /**
36 40 * Use for cache.
37 41 *
38 42 * @var User_Agent_Lockout
@@ -88,13 +92,10 @@
88 92 $model->log = esc_html__( 'Locked out due to attempted login with banned user agent', 'defender-security' );
89 93 break;
90 94 }
91 95 $model->save();
92 - // The 'defender_notify' hook doesn't work, so send notify directly.
93 - $module = wd_di()->get( Firewall_Notification::class );
94 - if ( $module->check_options( $model ) ) {
95 - $module->send( $model );
96 - }
96 + // Notify directly because the 'defender_notify' hook isn't registered yet at firewall-time.
97 + wd_di()->get( Firewall_Notification::class )->notify_lockout( $model );
97 98 }
98 99
99 100 /**
100 101 * Queue hooks when this class init.
@@ -118,22 +119,21 @@
118 119 *
119 120 * @return bool Returns true if the user agent is bad, false otherwise.
120 121 */
121 122 public function is_bad_user_agent( $user_agent ): bool {
122 - $allowlist = str_replace( '#', '\#', $this->model->get_lockout_list( 'allowlist' ) );
123 - $blocklist = str_replace( '#', '\#', $this->model->get_lockout_list( 'blocklist' ) );
124 -
123 + $allowlist = str_replace( '#', '\#', $this->model->get_lockout_list( 'allowlist' ) );
125 124 $allowlist_regex_pattern = '#' . implode( '|', $allowlist ) . '#i';
126 - $blocklist_regex_pattern = '#' . implode( '|', $blocklist ) . '#i';
125 + $allowlist_match = preg_match( $allowlist_regex_pattern, $user_agent );
127 126
128 - $allowlist_match = preg_match( $allowlist_regex_pattern, $user_agent );
129 - $blocklist_match = preg_match( $blocklist_regex_pattern, $user_agent );
130 -
131 - if ( count( $allowlist ) > 0 && ! empty( $allowlist_match ) ) {
127 + if ( count( $allowlist ) > 0 && $allowlist_match > 0 ) {
132 128 return false;
133 129 }
134 130
135 - if ( count( $blocklist ) > 0 && ! empty( $blocklist_match ) ) {
131 + $blocklist = str_replace( '#', '\#', $this->model->get_all_selected_blocklist_ua() );
132 + $blocklist_regex_pattern = '#' . implode( '|', $blocklist ) . '#i';
133 + $blocklist_match = preg_match( $blocklist_regex_pattern, $user_agent );
134 +
135 + if ( count( $blocklist ) > 0 && $blocklist_match > 0 ) {
136 136 return true;
137 137 }
138 138
139 139 return false;
@@ -144,9 +144,9 @@
144 144 *
145 145 * @return string The block message.
146 146 */
147 147 public function get_message(): string {
148 - return ! empty( $this->model->message )
148 + return '' !== $this->model->message
149 149 ? $this->model->message
150 150 : esc_html__( 'You have been blocked from accessing this website.', 'defender-security' );
151 151 }
152 152
@@ -162,8 +162,11 @@
162 162 do_action( 'wd_user_agent_before_block', $user_agent, $ip, $reason );
163 163 $this->log_event( $ip, $user_agent, $reason );
164 164 do_action( 'wd_user_agent_lockout', $this->model, self::SCENARIO_USER_AGENT_LOCKOUT );
165 165 // Shouldn't block IP via hook 'wd_blacklist_this_ip', block only when the button 'Ban IP' is clicked.
166 + if ( defender_is_wp_org_version() ) {
167 + Rate::run_counter_of_ua_lockouts();
168 + }
166 169 }
167 170
168 171 /**
169 172 * Cleans a user agent string quickly.
@@ -182,9 +185,9 @@
182 185 * @return string
183 186 */
184 187 public function sanitize_user_agent(): string {
185 188 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
186 - if ( empty( $user_agent ) ) {
189 + if ( '' === $user_agent ) {
187 190 return '';
188 191 }
189 192
190 193 $user_agent = apply_filters( 'wd_current_user_agent', $user_agent );
@@ -200,15 +203,15 @@
200 203 * @param string $user_agent The user agent of the request.
201 204 *
202 205 * @return bool Returns true if the headers are considered bad, false otherwise.
203 206 */
204 - public function is_bad_post( $user_agent ): bool {
207 + public function is_bad_post( string $user_agent ): bool {
205 208 $server = defender_get_data_from_request( null, 's' );
206 209
207 210 return true === $this->model->empty_headers
208 211 && 'POST' === $server['REQUEST_METHOD']
209 - && empty( $user_agent )
210 - && empty( $server['HTTP_REFERER'] );
212 + && '' === $user_agent
213 + && ( ! isset( $server['HTTP_REFERER'] ) || '' === $server['HTTP_REFERER'] );
211 214 }
212 215
213 216 /**
214 217 * Verifies the format and usability of an import file for User Agent Lockout settings.
@@ -219,9 +222,9 @@
219 222 */
220 223 public function verify_import_file( $file ) {
221 224 global $wp_filesystem;
222 225 // Initialize the WP filesystem, no more using 'file-put-contents' function.
223 - if ( empty( $wp_filesystem ) ) {
226 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
224 227 require_once ABSPATH . '/wp-admin/includes/file.php';
225 228 WP_Filesystem();
226 229 }
227 230 $contents = $wp_filesystem->get_contents( $file );
@@ -260,9 +263,9 @@
260 263 *
261 264 * @return string Human-readable text if log_type is UA else empty string.
262 265 */
263 266 public function get_status_text( $log_type, $user_agent ): string {
264 - if ( Lockout_Log::LOCKOUT_UA !== $log_type ) {
267 + if ( ! in_array( $log_type, Lockout_Log::get_ua_lockout_types(), true ) ) {
265 268 return '';
266 269 }
267 270
268 271 $status_text = self::EMPTY_USER_AGENT_TEXT;
@@ -272,9 +275,9 @@
272 275 }
273 276
274 277 $user_agent_key = $this->model->get_access_status( $user_agent );
275 278
276 - if ( ! empty( $user_agent_key[0] ) ) {
279 + if ( isset( $user_agent_key[0] ) && '' !== $user_agent_key[0] ) {
277 280 $status_text = $this->lockout_ip_model->get_access_status_text( $user_agent_key[0] );
278 281 }
279 282
280 283 return $status_text;
@@ -280,22 +283,104 @@
280 283 return $status_text;
281 284 }
282 285
283 286 /**
284 - * A list of known bad user agents.
287 + * Get Blocklist presets.
285 288 *
286 - * @return array An array of user agents.
289 + * @return array
287 290 */
288 - public static function get_spam_user_agent_list() {
291 + public static function get_blocklist_presets(): array {
289 292 return array(
290 - 'AhrefsBot',
291 - 'DotBot',
292 - 'EmailSiphon',
293 - 'HTTrack',
294 - 'MJ12Bot',
295 - 'Nmap',
296 - 'SEMrushBot',
297 - 'sqlmap',
298 - 'ZmEu',
293 + 'brute_forcing_tools' => array(
294 + 'feroxbuster' => 'Feroxbuster',
295 + 'gobuster' => 'Gobuster',
296 + ),
297 + 'security_scanners' => array(
298 + 'sqlmap' => 'SQLMap',
299 + 'wfuzz' => 'Wfuzz',
300 + ),
301 + 'seo_crawlers' => array(
302 + 'dotbot' => 'DotBot (Moz)',
303 + 'mj12bot' => 'MJ12Bot (Majestic)',
304 + 'ahrefsbot' => 'AhrefsBot',
305 + 'semrushbot' => 'SEMrushBot',
306 + 'thinkbot' => 'Thinkbot',
307 + ),
299 308 );
309 + }
310 +
311 + /**
312 + * Get only keys of nested Blocklist preset arrays.
313 + *
314 + * @return array
315 + */
316 + public static function get_nested_keys_of_blocklist_presets(): array {
317 + $all_keys = array();
318 + $presets = self::get_blocklist_presets();
319 + foreach ( $presets as $category => $tools ) {
320 + foreach ( $tools as $key => $value ) {
321 + $all_keys[] = $key;
322 + }
323 + }
324 +
325 + return $all_keys;
326 + }
327 +
328 + /**
329 + * Is the current UA in the Blocklist preset list?
330 + *
331 + * @param string $key User Agent key.
332 + *
333 + * @return bool
334 + */
335 + public static function is_blocklist_presets( $key ): bool {
336 + return in_array( $key, self::get_nested_keys_of_blocklist_presets(), true );
337 + }
338 +
339 + /**
340 + * Get Script presets.
341 + *
342 + * @return array
343 + */
344 + public static function get_script_presets(): array {
345 + return array(
346 + self::PYTHON_REQUESTS_KEY => array(
347 + 'label' => 'Python Script',
348 + 'desc' => __( '(blocks python-requests/* agent)', 'defender-security' ),
349 + ),
350 + self::GO_HTTP_CLIENT_KEY => array(
351 + 'label' => 'Go HTTP Clients',
352 + 'desc' => __( '(blocks Go-http-client/* agent)', 'defender-security' ),
353 + ),
354 + );
355 + }
356 +
357 + /**
358 + * Is the current UA in the Script preset list?
359 + *
360 + * @param string $key User Agent key.
361 + *
362 + * @return bool
363 + */
364 + public static function is_script_presets( $key ): bool {
365 + return in_array( $key, array_keys( self::get_script_presets() ), true );
366 + }
367 +
368 + /**
369 + * Check and remove duplicates in passed UA array.
370 + *
371 + * @param array $arr_source Source array.
372 + * @param array $arr_search Search array.
373 + *
374 + * @return array
375 + */
376 + public static function check_and_remove_duplicates( $arr_source, $arr_search ): array {
377 + foreach ( $arr_search as $ua ) {
378 + $key = array_search( $ua, $arr_source, true );
379 + if ( false !== $key ) {
380 + unset( $arr_source[ $key ] );
381 + }
382 + }
383 +
384 + return is_array( $arr_source ) && array() !== $arr_source ? array_values( $arr_source ) : array();
300 385 }
301 386 }