PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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
defender-security / src / component / class-user-agent.php

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

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