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

302 lines 8.2 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 /**
36 * Use for cache.
37 *
38 * @var User_Agent_Lockout
39 */
40 protected $model;
41
42 /**
43 * Lockout IP model instance.
44 *
45 * @var Lockout_Ip
46 */
47 protected $lockout_ip_model;
48
49 /**
50 * Initializes the User_Agent component with necessary models.
51 */
52 public function __construct() {
53 $this->model = wd_di()->get( User_Agent_Lockout::class );
54 $this->lockout_ip_model = wd_di()->get( Lockout_Ip::class );
55 }
56
57 /**
58 * Logs a user agent event into the database.
59 *
60 * @param string $ip The IP address associated with the event.
61 * @param string $user_agent The user agent string associated with the event.
62 * @param string $reason The reason for the event.
63 */
64 private function log_event( $ip, $user_agent, $reason ) {
65 $model = new Lockout_Log();
66 $model->ip = $ip;
67 $model->user_agent = $user_agent;
68 $model->date = time();
69 $model->tried = $user_agent;
70 $model->blog_id = get_current_blog_id();
71 $model->type = Lockout_Log::LOCKOUT_UA;
72
73 $ip_to_country = $this->ip_to_country( $ip );
74
75 if ( isset( $ip_to_country['iso'] ) ) {
76 $model->country_iso_code = $ip_to_country['iso'];
77 }
78
79 switch ( $reason ) {
80 case self::REASON_BAD_POST:
81 // Distinguish between different block cases of User agent lockouts.
82 $model->tried = self::REASON_BAD_POST;
83 $model->log = esc_html__( 'Locked out due to empty User-Agent and Referer headers', 'defender-security' );
84 break;
85 case self::REASON_BAD_USER_AGENT:
86 default:
87 $model->tried = $user_agent;
88 $model->log = esc_html__( 'Locked out due to attempted login with banned user agent', 'defender-security' );
89 break;
90 }
91 $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 }
97 }
98
99 /**
100 * Queue hooks when this class init.
101 */
102 public function add_hooks() {
103 }
104
105 /**
106 * Checks if the User_Agent component is active.
107 *
108 * @return bool Returns true if the component is active, false otherwise.
109 */
110 public function is_active_component(): bool {
111 return $this->model->is_active() && ! is_admin();
112 }
113
114 /**
115 * Determines if the provided user agent is considered bad.
116 *
117 * @param string $user_agent The user agent to check.
118 *
119 * @return bool Returns true if the user agent is bad, false otherwise.
120 */
121 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
125 $allowlist_regex_pattern = '#' . implode( '|', $allowlist ) . '#i';
126 $blocklist_regex_pattern = '#' . implode( '|', $blocklist ) . '#i';
127
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 ) ) {
132 return false;
133 }
134
135 if ( count( $blocklist ) > 0 && ! empty( $blocklist_match ) ) {
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 ! empty( $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 }
167
168 /**
169 * Cleans a user agent string quickly.
170 *
171 * @param string $user_agent The user agent string to clean.
172 *
173 * @return string The cleaned user agent string.
174 */
175 public static function fast_cleaning( $user_agent ): string {
176 return trim( sanitize_text_field( $user_agent ) );
177 }
178
179 /**
180 * Sanitize User Agent.
181 *
182 * @return string
183 */
184 public function sanitize_user_agent(): string {
185 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
186 if ( empty( $user_agent ) ) {
187 return '';
188 }
189
190 $user_agent = apply_filters( 'wd_current_user_agent', $user_agent );
191 $user_agent = self::fast_cleaning( $user_agent );
192 $user_agent = strtolower( $user_agent );
193
194 return $user_agent;
195 }
196
197 /**
198 * Checks if the POST request has blank User-Agent and Referer headers.
199 *
200 * @param string $user_agent The user agent of the request.
201 *
202 * @return bool Returns true if the headers are considered bad, false otherwise.
203 */
204 public function is_bad_post( $user_agent ): bool {
205 $server = defender_get_data_from_request( null, 's' );
206
207 return true === $this->model->empty_headers
208 && 'POST' === $server['REQUEST_METHOD']
209 && empty( $user_agent )
210 && empty( $server['HTTP_REFERER'] );
211 }
212
213 /**
214 * Verifies the format and usability of an import file for User Agent Lockout settings.
215 *
216 * @param string $file The file path to verify.
217 *
218 * @return array|bool Returns the data if the file is valid, false otherwise.
219 */
220 public function verify_import_file( $file ) {
221 global $wp_filesystem;
222 // Initialize the WP filesystem, no more using 'file-put-contents' function.
223 if ( empty( $wp_filesystem ) ) {
224 require_once ABSPATH . '/wp-admin/includes/file.php';
225 WP_Filesystem();
226 }
227 $contents = $wp_filesystem->get_contents( $file );
228 $lines = explode( "\n", $contents );
229 $data = array();
230 foreach ( $lines as $line ) {
231 if ( '' === $line ) {
232 continue;
233 }
234 $line = str_getcsv( $line, ',', '"', '\\' );
235 if ( count( $line ) !== 2 ) {
236 return false;
237 }
238 if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) {
239 return false;
240 }
241
242 $ua = $line[0];
243 $ua = self::fast_cleaning( $ua );
244
245 if ( '' === $ua ) {
246 continue;
247 }
248 $line[0] = $ua;
249
250 $data[] = $line;
251 }
252 return $data;
253 }
254
255 /**
256 * Get human readable user agent log status text.
257 *
258 * @param string $log_type Type of the log. Handles on 'ua_lockout'.
259 * @param string $user_agent User Agent name.
260 *
261 * @return string Human-readable text if log_type is UA else empty string.
262 */
263 public function get_status_text( $log_type, $user_agent ): string {
264 if ( Lockout_Log::LOCKOUT_UA !== $log_type ) {
265 return '';
266 }
267
268 $status_text = self::EMPTY_USER_AGENT_TEXT;
269
270 if ( self::REASON_BAD_POST === $user_agent ) {
271 return $status_text;
272 }
273
274 $user_agent_key = $this->model->get_access_status( $user_agent );
275
276 if ( ! empty( $user_agent_key[0] ) ) {
277 $status_text = $this->lockout_ip_model->get_access_status_text( $user_agent_key[0] );
278 }
279
280 return $status_text;
281 }
282
283 /**
284 * A list of known bad user agents.
285 *
286 * @return array An array of user agents.
287 */
288 public static function get_spam_user_agent_list() {
289 return array(
290 'AhrefsBot',
291 'DotBot',
292 'EmailSiphon',
293 'HTTrack',
294 'MJ12Bot',
295 'Nmap',
296 'SEMrushBot',
297 'sqlmap',
298 'ZmEu',
299 );
300 }
301 }
302