PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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-user-agent.php

class-user-agent.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/component/class-user-agent.php

389 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles User-Agent based operations including lockouts and logging for security purposes.
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 use WP_Defender\Model\Setting\User_Agent_Lockout;
15 use WP_Defender\Model\Notification\Firewall_Notification;
16 use WP_Filesystem_Base;
17
18 /**
19 * Handles User-Agent based operations including lockouts and logging for security purposes.
20 *
21 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
22 * @since 2.6.0
23 */
24 class User_Agent extends Component {
25
26 use Country;
27
28 public const SCENARIO_USER_AGENT_LOCKOUT = 'user_agent_lockout';
29 public const REASON_BAD_USER_AGENT = 'bad_user_agent', REASON_BAD_POST = 'bad_post';
30
31 /**
32 * Human Readable text denotes user agent header is empty.
33 */
34 public const EMPTY_USER_AGENT_TEXT = 'Empty User Agent';
35
36 public const GO_HTTP_CLIENT_KEY = 'go-http-client';
37 public const PYTHON_REQUESTS_KEY = 'python-requests';
38
39 /**
40 * Use for cache.
41 *
42 * @var User_Agent_Lockout
43 */
44 protected $model;
45
46 /**
47 * Lockout IP model instance.
48 *
49 * @var Lockout_Ip
50 */
51 protected $lockout_ip_model;
52
53 /**
54 * Initializes the User_Agent component with necessary models.
55 */
56 public function __construct() {
57 $this->model = wd_di()->get( User_Agent_Lockout::class );
58 $this->lockout_ip_model = wd_di()->get( Lockout_Ip::class );
59 }
60
61 /**
62 * Logs a user agent event into the database.
63 *
64 * @param string $ip The IP address associated with the event.
65 * @param string $user_agent The user agent string associated with the event.
66 * @param string $reason The reason for the event.
67 */
68 private function log_event( $ip, $user_agent, $reason ) {
69 $model = new Lockout_Log();
70 $model->ip = $ip;
71 $model->user_agent = $user_agent;
72 $model->date = time();
73 $model->tried = $user_agent;
74 $model->blog_id = get_current_blog_id();
75 $model->type = Lockout_Log::LOCKOUT_UA;
76
77 $ip_to_country = $this->ip_to_country( $ip );
78
79 if ( isset( $ip_to_country['iso'] ) ) {
80 $model->country_iso_code = $ip_to_country['iso'];
81 }
82
83 switch ( $reason ) {
84 case self::REASON_BAD_POST:
85 // Distinguish between different block cases of User agent lockouts.
86 $model->tried = self::REASON_BAD_POST;
87 $model->log = esc_html__( 'Locked out due to empty User-Agent and Referer headers', 'defender-security' );
88 break;
89 case self::REASON_BAD_USER_AGENT:
90 default:
91 $model->tried = $user_agent;
92 $model->log = esc_html__( 'Locked out due to attempted login with banned user agent', 'defender-security' );
93 break;
94 }
95 $model->save();
96 // The 'defender_notify' hook doesn't work, so send notify directly.
97 $module = wd_di()->get( Firewall_Notification::class );
98 if ( $module->check_options( $model ) ) {
99 $module->send( $model );
100 }
101 }
102
103 /**
104 * Queue hooks when this class init.
105 */
106 public function add_hooks() {
107 }
108
109 /**
110 * Checks if the User_Agent component is active.
111 *
112 * @return bool Returns true if the component is active, false otherwise.
113 */
114 public function is_active_component(): bool {
115 return $this->model->is_active() && ! is_admin();
116 }
117
118 /**
119 * Determines if the provided user agent is considered bad.
120 *
121 * @param string $user_agent The user agent to check.
122 *
123 * @return bool Returns true if the user agent is bad, false otherwise.
124 */
125 public function is_bad_user_agent( $user_agent ): bool {
126 $allowlist = str_replace( '#', '\#', $this->model->get_lockout_list( 'allowlist' ) );
127 $allowlist_regex_pattern = '#' . implode( '|', $allowlist ) . '#i';
128 $allowlist_match = preg_match( $allowlist_regex_pattern, $user_agent );
129
130 if ( count( $allowlist ) > 0 && ! empty( $allowlist_match ) ) {
131 return false;
132 }
133
134 $blocklist = str_replace( '#', '\#', $this->model->get_all_selected_blocklist_ua() );
135 $blocklist_regex_pattern = '#' . implode( '|', $blocklist ) . '#i';
136 $blocklist_match = preg_match( $blocklist_regex_pattern, $user_agent );
137
138 if ( count( $blocklist ) > 0 && ! empty( $blocklist_match ) ) {
139 return true;
140 }
141
142 return false;
143 }
144
145 /**
146 * Retrieves the message to display for blocked user agents.
147 *
148 * @return string The block message.
149 */
150 public function get_message(): string {
151 return ! empty( $this->model->message )
152 ? $this->model->message
153 : esc_html__( 'You have been blocked from accessing this website.', 'defender-security' );
154 }
155
156 /**
157 * Blocks a user agent or IP and logs the event.
158 *
159 * @param string $user_agent The user agent to block.
160 * @param string $ip The IP address to block.
161 * @param string $reason The reason for blocking.
162 */
163 public function block_user_agent_or_ip( $user_agent, $ip, $reason ) {
164 // since 2.6.0.
165 do_action( 'wd_user_agent_before_block', $user_agent, $ip, $reason );
166 $this->log_event( $ip, $user_agent, $reason );
167 do_action( 'wd_user_agent_lockout', $this->model, self::SCENARIO_USER_AGENT_LOCKOUT );
168 // Shouldn't block IP via hook 'wd_blacklist_this_ip', block only when the button 'Ban IP' is clicked.
169 if ( defender_is_wp_org_version() ) {
170 Rate::run_counter_of_ua_lockouts();
171 }
172 }
173
174 /**
175 * Cleans a user agent string quickly.
176 *
177 * @param string $user_agent The user agent string to clean.
178 *
179 * @return string The cleaned user agent string.
180 */
181 public static function fast_cleaning( $user_agent ): string {
182 return trim( sanitize_text_field( $user_agent ) );
183 }
184
185 /**
186 * Sanitize User Agent.
187 *
188 * @return string
189 */
190 public function sanitize_user_agent(): string {
191 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
192 if ( empty( $user_agent ) ) {
193 return '';
194 }
195
196 $user_agent = apply_filters( 'wd_current_user_agent', $user_agent );
197 $user_agent = self::fast_cleaning( $user_agent );
198 $user_agent = strtolower( $user_agent );
199
200 return $user_agent;
201 }
202
203 /**
204 * Checks if the POST request has blank User-Agent and Referer headers.
205 *
206 * @param string $user_agent The user agent of the request.
207 *
208 * @return bool Returns true if the headers are considered bad, false otherwise.
209 */
210 public function is_bad_post( $user_agent ): bool {
211 $server = defender_get_data_from_request( null, 's' );
212
213 return true === $this->model->empty_headers
214 && 'POST' === $server['REQUEST_METHOD']
215 && empty( $user_agent )
216 && empty( $server['HTTP_REFERER'] );
217 }
218
219 /**
220 * Verifies the format and usability of an import file for User Agent Lockout settings.
221 *
222 * @param string $file The file path to verify.
223 *
224 * @return array|bool Returns the data if the file is valid, false otherwise.
225 */
226 public function verify_import_file( $file ) {
227 global $wp_filesystem;
228 // Initialize the WP filesystem, no more using 'file-put-contents' function.
229 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
230 require_once ABSPATH . '/wp-admin/includes/file.php';
231 WP_Filesystem();
232 }
233 $contents = $wp_filesystem->get_contents( $file );
234 $lines = explode( "\n", $contents );
235 $data = array();
236 foreach ( $lines as $line ) {
237 if ( '' === $line ) {
238 continue;
239 }
240 $line = str_getcsv( $line, ',', '"', '\\' );
241 if ( count( $line ) !== 2 ) {
242 return false;
243 }
244 if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) {
245 return false;
246 }
247
248 $ua = $line[0];
249 $ua = self::fast_cleaning( $ua );
250
251 if ( '' === $ua ) {
252 continue;
253 }
254 $line[0] = $ua;
255
256 $data[] = $line;
257 }
258 return $data;
259 }
260
261 /**
262 * Get human readable user agent log status text.
263 *
264 * @param string $log_type Type of the log. Handles on 'ua_lockout'.
265 * @param string $user_agent User Agent name.
266 *
267 * @return string Human-readable text if log_type is UA else empty string.
268 */
269 public function get_status_text( $log_type, $user_agent ): string {
270 if ( ! in_array( $log_type, Lockout_Log::get_ua_lockout_types(), true ) ) {
271 return '';
272 }
273
274 $status_text = self::EMPTY_USER_AGENT_TEXT;
275
276 if ( self::REASON_BAD_POST === $user_agent ) {
277 return $status_text;
278 }
279
280 $user_agent_key = $this->model->get_access_status( $user_agent );
281
282 if ( ! empty( $user_agent_key[0] ) ) {
283 $status_text = $this->lockout_ip_model->get_access_status_text( $user_agent_key[0] );
284 }
285
286 return $status_text;
287 }
288
289 /**
290 * Get Blocklist presets.
291 *
292 * @return array
293 */
294 public static function get_blocklist_presets(): array {
295 return array(
296 'brute_forcing_tools' => array(
297 'feroxbuster' => 'Feroxbuster',
298 'gobuster' => 'Gobuster',
299 ),
300 'security_scanners' => array(
301 'sqlmap' => 'SQLMap',
302 'wfuzz' => 'Wfuzz',
303 ),
304 'seo_crawlers' => array(
305 'dotbot' => 'DotBot (Moz)',
306 'mj12bot' => 'MJ12Bot (Majestic)',
307 'ahrefsbot' => 'AhrefsBot',
308 'semrushbot' => 'SEMrushBot',
309 ),
310 );
311 }
312
313 /**
314 * Get only keys of nested Blocklist preset arrays.
315 *
316 * @return array
317 */
318 public static function get_nested_keys_of_blocklist_presets(): array {
319 $all_keys = array();
320 $presets = self::get_blocklist_presets();
321 foreach ( $presets as $category => $tools ) {
322 foreach ( $tools as $key => $value ) {
323 $all_keys[] = $key;
324 }
325 }
326
327 return $all_keys;
328 }
329
330 /**
331 * Is the current UA in the Blocklist preset list?
332 *
333 * @param string $key User Agent key.
334 *
335 * @return bool
336 */
337 public static function is_blocklist_presets( $key ): bool {
338 return in_array( $key, self::get_nested_keys_of_blocklist_presets(), true );
339 }
340
341 /**
342 * Get Script presets.
343 *
344 * @return array
345 */
346 public static function get_script_presets(): array {
347 return array(
348 self::PYTHON_REQUESTS_KEY => array(
349 'label' => 'Python Script',
350 'desc' => __( '( This will block all requests from python-requests/* agent )', 'defender-security' ),
351 ),
352 self::GO_HTTP_CLIENT_KEY => array(
353 'label' => 'Go Http Clients',
354 'desc' => __( '( This will block all requests from Go-http-client/* agent )', 'defender-security' ),
355 ),
356 );
357 }
358
359 /**
360 * Is the current UA in the Script preset list?
361 *
362 * @param string $key User Agent key.
363 *
364 * @return bool
365 */
366 public static function is_script_presets( $key ): bool {
367 return in_array( $key, array_keys( self::get_script_presets() ), true );
368 }
369
370 /**
371 * Check and remove duplicates in passed UA array.
372 *
373 * @param array $arr_source Source array.
374 * @param array $arr_search Search array.
375 *
376 * @return array
377 */
378 public static function check_and_remove_duplicates( $arr_source, $arr_search ): array {
379 foreach ( $arr_search as $ua ) {
380 $key = array_search( $ua, $arr_source, true );
381 if ( false !== $key ) {
382 unset( $arr_source[ $key ] );
383 }
384 }
385
386 return ! empty( $arr_source ) ? array_values( $arr_source ) : array();
387 }
388 }
389