PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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 6.2.4, at src/component/class-user-agent.php

387 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 // Notify directly because the 'defender_notify' hook isn't registered yet at firewall-time.
97 wd_di()->get( Firewall_Notification::class )->notify_lockout( $model );
98 }
99
100 /**
101 * Queue hooks when this class init.
102 */
103 public function add_hooks() {
104 }
105
106 /**
107 * Checks if the User_Agent component is active.
108 *
109 * @return bool Returns true if the component is active, false otherwise.
110 */
111 public function is_active_component(): bool {
112 return $this->model->is_active() && ! is_admin();
113 }
114
115 /**
116 * Determines if the provided user agent is considered bad.
117 *
118 * @param string $user_agent The user agent to check.
119 *
120 * @return bool Returns true if the user agent is bad, false otherwise.
121 */
122 public function is_bad_user_agent( $user_agent ): bool {
123 $allowlist = str_replace( '#', '\#', $this->model->get_lockout_list( 'allowlist' ) );
124 $allowlist_regex_pattern = '#' . implode( '|', $allowlist ) . '#i';
125 $allowlist_match = preg_match( $allowlist_regex_pattern, $user_agent );
126
127 if ( count( $allowlist ) > 0 && $allowlist_match > 0 ) {
128 return false;
129 }
130
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 return true;
137 }
138
139 return false;
140 }
141
142 /**
143 * Retrieves the message to display for blocked user agents.
144 *
145 * @return string The block message.
146 */
147 public function get_message(): string {
148 return '' !== $this->model->message
149 ? $this->model->message
150 : esc_html__( 'You have been blocked from accessing this website.', 'defender-security' );
151 }
152
153 /**
154 * Blocks a user agent or IP and logs the event.
155 *
156 * @param string $user_agent The user agent to block.
157 * @param string $ip The IP address to block.
158 * @param string $reason The reason for blocking.
159 */
160 public function block_user_agent_or_ip( $user_agent, $ip, $reason ) {
161 // since 2.6.0.
162 do_action( 'wd_user_agent_before_block', $user_agent, $ip, $reason );
163 $this->log_event( $ip, $user_agent, $reason );
164 do_action( 'wd_user_agent_lockout', $this->model, self::SCENARIO_USER_AGENT_LOCKOUT );
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 }
169 }
170
171 /**
172 * Cleans a user agent string quickly.
173 *
174 * @param string $user_agent The user agent string to clean.
175 *
176 * @return string The cleaned user agent string.
177 */
178 public static function fast_cleaning( $user_agent ): string {
179 return trim( sanitize_text_field( $user_agent ) );
180 }
181
182 /**
183 * Sanitize User Agent.
184 *
185 * @return string
186 */
187 public function sanitize_user_agent(): string {
188 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
189 if ( '' === $user_agent ) {
190 return '';
191 }
192
193 $user_agent = apply_filters( 'wd_current_user_agent', $user_agent );
194 $user_agent = self::fast_cleaning( $user_agent );
195 $user_agent = strtolower( $user_agent );
196
197 return $user_agent;
198 }
199
200 /**
201 * Checks if the POST request has blank User-Agent and Referer headers.
202 *
203 * @param string $user_agent The user agent of the request.
204 *
205 * @return bool Returns true if the headers are considered bad, false otherwise.
206 */
207 public function is_bad_post( string $user_agent ): bool {
208 $server = defender_get_data_from_request( null, 's' );
209
210 return true === $this->model->empty_headers
211 && 'POST' === $server['REQUEST_METHOD']
212 && '' === $user_agent
213 && ( ! isset( $server['HTTP_REFERER'] ) || '' === $server['HTTP_REFERER'] );
214 }
215
216 /**
217 * Verifies the format and usability of an import file for User Agent Lockout settings.
218 *
219 * @param string $file The file path to verify.
220 *
221 * @return array|bool Returns the data if the file is valid, false otherwise.
222 */
223 public function verify_import_file( $file ) {
224 global $wp_filesystem;
225 // Initialize the WP filesystem, no more using 'file-put-contents' function.
226 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
227 require_once ABSPATH . '/wp-admin/includes/file.php';
228 WP_Filesystem();
229 }
230 $contents = $wp_filesystem->get_contents( $file );
231 $lines = explode( "\n", $contents );
232 $data = array();
233 foreach ( $lines as $line ) {
234 if ( '' === $line ) {
235 continue;
236 }
237 $line = str_getcsv( $line, ',', '"', '\\' );
238 if ( count( $line ) !== 2 ) {
239 return false;
240 }
241 if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) {
242 return false;
243 }
244
245 $ua = $line[0];
246 $ua = self::fast_cleaning( $ua );
247
248 if ( '' === $ua ) {
249 continue;
250 }
251 $line[0] = $ua;
252
253 $data[] = $line;
254 }
255 return $data;
256 }
257
258 /**
259 * Get human readable user agent log status text.
260 *
261 * @param string $log_type Type of the log. Handles on 'ua_lockout'.
262 * @param string $user_agent User Agent name.
263 *
264 * @return string Human-readable text if log_type is UA else empty string.
265 */
266 public function get_status_text( $log_type, $user_agent ): string {
267 if ( ! in_array( $log_type, Lockout_Log::get_ua_lockout_types(), true ) ) {
268 return '';
269 }
270
271 $status_text = self::EMPTY_USER_AGENT_TEXT;
272
273 if ( self::REASON_BAD_POST === $user_agent ) {
274 return $status_text;
275 }
276
277 $user_agent_key = $this->model->get_access_status( $user_agent );
278
279 if ( isset( $user_agent_key[0] ) && '' !== $user_agent_key[0] ) {
280 $status_text = $this->lockout_ip_model->get_access_status_text( $user_agent_key[0] );
281 }
282
283 return $status_text;
284 }
285
286 /**
287 * Get Blocklist presets.
288 *
289 * @return array
290 */
291 public static function get_blocklist_presets(): array {
292 return array(
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 ),
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();
385 }
386 }
387