PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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-session-protection.php

class-session-protection.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at src/component/class-session-protection.php

473 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles session protection functionality within the WP Defender plugin.
4 *
5 * @package WP_Defender\Component
6 * @since 5.2.0
7 */
8
9 namespace WP_Defender\Component;
10
11 use WP_Defender\Component;
12 use WP_Defender\Traits\IP;
13 use WP_Defender\Traits\User;
14 use WP_User_Meta_Session_Tokens;
15 use WP_Defender\Model\Setting\Session_Protection as Settings;
16
17 /**
18 * This class handles session protection functionality within the WP Defender plugin.
19 */
20 class Session_Protection extends Component {
21 use IP;
22 use User;
23
24 /**
25 * Key to lock the session to the browser.
26 */
27 const LOCK_BROWSER = 'browser';
28
29 /**
30 * Key to lock the session to the hostname.
31 */
32 const LOCK_HOSTNAME = 'hostname';
33
34 /**
35 * Key to lock the session to the IP address.
36 */
37 const LOCK_IP_ADDRESS = 'ip_address';
38
39 /**
40 * The name of the log file.
41 *
42 * @var string
43 */
44 const LOG_FILE_NAME = 'session-protection';
45
46 /**
47 * Transient key to show custom logout message in the login modal.
48 *
49 * @var string
50 */
51 const LOGOUT_MSG_TRANSIENT_KEY = 'wpdef_session_logout_msg';
52
53 /**
54 * The session protection settings.
55 *
56 * @var Settings|null
57 */
58 protected ?Settings $settings;
59
60 /**
61 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
62 *
63 * @return void
64 */
65 public function __construct() {
66 $this->settings = wd_di()->get( Settings::class );
67 }
68
69 /**
70 * Retrieve an array of default session lock properties.
71 *
72 * @return array An array of session lock properties.
73 */
74 public static function session_lock_properties(): array {
75 return array(
76 self::LOCK_BROWSER => esc_html__( 'Browser', 'defender-security' ),
77 self::LOCK_HOSTNAME => esc_html__( 'Hostname', 'defender-security' ),
78 self::LOCK_IP_ADDRESS => esc_html__( 'IP Address', 'defender-security' ),
79 );
80 }
81
82 /**
83 * Prepare details of the Session Lock property for MP event.
84 * No need to translate.
85 *
86 * @return string
87 */
88 public function get_session_lock_string(): string {
89 if ( ! $this->settings->has_properties() ) {
90 return 'Not Set';
91 }
92 $checked_properties = array();
93 if ( $this->settings->is_property_locked( self::LOCK_IP_ADDRESS ) ) {
94 $checked_properties[] = 'IP';
95 }
96 if ( $this->settings->is_property_locked( self::LOCK_HOSTNAME ) ) {
97 $checked_properties[] = 'Hostname';
98 }
99 if ( $this->settings->is_property_locked( self::LOCK_BROWSER ) ) {
100 $checked_properties[] = 'Browser';
101 }
102
103 return implode( '/', $checked_properties );
104 }
105
106 /**
107 * Handles session timeout logic on init hook.
108 *
109 * @return void
110 */
111 public function handle_session_timeout() {
112 if (
113 ! is_user_logged_in()
114 || wp_doing_ajax()
115 || wp_doing_cron()
116 || defender_is_rest_api_request()
117 || $this->request_is_from_server()
118 ) {
119 return;
120 }
121
122 if ( ! $this->can_apply_session_protection() ) {
123 return;
124 }
125
126 if ( $this->settings->has_properties() ) {
127 $this->handle_session_lock();
128 return;
129 }
130
131 $idle_timeout = $this->get_idle_timeout();
132 $last_activity = $this->get_last_activity();
133
134 if ( $last_activity && ( defender_get_current_time() - $last_activity > $idle_timeout ) ) {
135 $this->log(
136 sprintf( 'User session timed out due to inactivity during %s hours', $this->settings->idle_timeout ),
137 self::LOG_FILE_NAME
138 );
139 $user_id = get_current_user_id();
140 // Fires during session timeout.
141 do_action( 'wpdef_session_timeout', $user_id );
142 $this->logout( $user_id );
143
144 return;
145 }
146 $this->update_last_activity();
147 }
148
149 /**
150 * Retrieves the last activity timestamp.
151 *
152 * @return int|null Last activity timestamp or null if not set.
153 */
154 private function get_last_activity() {
155 // Check for client-side cookie first to avoid unnecessary database queries.
156 if ( isset( $_COOKIE['wpdef_last_activity'] ) && is_numeric( $_COOKIE['wpdef_last_activity'] ) ) {
157 return (int) $_COOKIE['wpdef_last_activity'];
158 }
159
160 return null;
161 }
162
163 /**
164 * Logs out the current user due to inactivity.
165 *
166 * @param int|null $user_id User ID.
167 *
168 * @return void
169 */
170 public function logout( $user_id = null ) {
171 if ( is_user_logged_in() ) {
172 // Case without $user_id may be from Ajax.
173 if ( is_null( $user_id ) ) {
174 $user_id = get_current_user_id();
175 $this->log(
176 sprintf( 'User session timed out due to inactivity during %s hours', $this->settings->idle_timeout ),
177 self::LOG_FILE_NAME
178 );
179 // Fires during session timeout.
180 do_action( 'wpdef_session_timeout', $user_id );
181 }
182 // Get current session token.
183 $current_session_token = wp_get_session_token();
184 if ( $current_session_token ) {
185 // Get session manager for the user.
186 $session_manager = \WP_Session_Tokens::get_instance( $user_id );
187
188 // Destroy ONLY the current session.
189 $session_manager->destroy( $current_session_token );
190 } else {
191 $manager = \WP_Session_Tokens::get_instance( $user_id );
192 $manager->destroy_all();
193 }
194
195 // Reset last activity cookie.
196 setcookie( 'wpdef_last_activity', '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
197
198 // Set transient to show custom message in the login modal.
199 set_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY, 1, 30 );
200
201 // Redirect to login page after session destruction.
202 wp_safe_redirect( wp_login_url() );
203 }
204 }
205
206 /**
207 * Show custom message in the login modal when session is expired.
208 *
209 * @param \WP_Error $errors WP Error object.
210 *
211 * @return \WP_Error
212 */
213 public function login_modal_message( $errors ) {
214 $interim_login = defender_get_data_from_request( 'interim-login', 'g' );
215 if ( is_null( $interim_login ) || '' === $interim_login || 1 !== get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) ) {
216 return $errors;
217 }
218
219 delete_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY );
220
221 if ( ! ( $errors instanceof \WP_Error ) ) {
222 return $errors;
223 }
224
225 $error_keys = array( 'expired', 'expired_session' );
226 foreach ( $error_keys as $key ) {
227 if ( $errors->get_error_message( $key ) ) {
228 $errors->remove( $key );
229 $errors->add(
230 $key,
231 __( 'You\'ve been logged out due to inactivity. Please sign in to continue.', 'defender-security' ),
232 'message'
233 );
234
235 break;
236 }
237 }
238
239 return $errors;
240 }
241
242 /**
243 * Styles for the custom message in the login modal.
244 *
245 * @return void
246 */
247 public function login_modal_message_styles(): void {
248 if ( 1 === get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) ) {
249 // Don't delete the transient here, it will be deleted in the login_modal_message() method.
250
251 echo '<style>
252 #login_error {
253 border-left-color: #72aee6;
254 }
255 </style>';
256 }
257 }
258
259 /**
260 * Enqueues JavaScript for client-side idle detection.
261 *
262 * @return void
263 */
264 public function enqueue_idle_scripts() {
265 if ( is_user_logged_in() && $this->can_apply_session_protection() ) {
266 wp_enqueue_script( 'wpdef-session-idle', plugins_url( 'assets/js/idle-session.js', WP_DEFENDER_FILE ), array( 'jquery' ), DEFENDER_VERSION, true );
267 wp_localize_script(
268 'wpdef-session-idle',
269 'wpdef_idle_params',
270 array(
271 'timeout' => $this->get_idle_timeout(),
272 'ajax_url' => admin_url( 'admin-ajax.php' ),
273 'login_url' => wp_login_url(),
274 // Exclude sending Logout-callbacks if the user is logged out and the login form is displayed in the popup.
275 'stop' => false,
276 )
277 );
278 }
279 }
280
281 /**
282 * Retrieves the idle timeout duration.
283 *
284 * @return int The idle timeout duration in seconds.
285 */
286 public function get_idle_timeout() {
287 /**
288 * Filter the idle timeout duration in seconds.
289 *
290 * @param int $idle_timeout The idle timeout duration in seconds.
291 *
292 * @return int The idle timeout duration in seconds.
293 */
294 $idle_timeout = apply_filters( 'wpdef_idle_timeout', $this->settings->idle_timeout * HOUR_IN_SECONDS );
295 return ! is_int( $idle_timeout ) ? (int) $idle_timeout : $idle_timeout;
296 }
297
298 /**
299 * Updates the last activity timestamp efficiently.
300 *
301 * @return void
302 */
303 public function update_last_activity() {
304 $current_time = time();
305 $last_activity = $this->get_last_activity();
306
307 // Only update the cookie if minutes have passed since the last update.
308 if ( ! is_null( $last_activity ) || ( $current_time - $last_activity > MINUTE_IN_SECONDS ) ) {
309 setcookie( 'wpdef_last_activity', $current_time, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
310 }
311 }
312
313 /**
314 * Attach session information to the current user session.
315 *
316 * @param array $session Array of extra data.
317 *
318 * @return array Array of extra data.
319 */
320 public function attach_session_information( $session ) {
321 if ( $this->settings->is_property_locked( self::LOCK_IP_ADDRESS ) ) {
322 $session['defender_ip'] = implode( '|', $this->get_user_ip() );
323 }
324
325 if ( $this->settings->is_property_locked( self::LOCK_HOSTNAME ) ) {
326 $session['defender_hostname'] = defender_get_domain();
327 }
328
329 if ( $this->settings->is_property_locked( self::LOCK_BROWSER ) ) {
330 $session['defender_browser'] = defender_get_user_agent();
331 }
332
333 return $session;
334 }
335
336 /**
337 * Handles session lock logic.
338 *
339 * @return void
340 */
341 public function handle_session_lock() {
342 $token = wp_get_session_token();
343 $user_id = get_current_user_id();
344 $manager = WP_User_Meta_Session_Tokens::get_instance( $user_id );
345 $session_data = $manager->get( $token );
346 if ( ! is_array( $session_data ) ) {
347 $this->log( 'Session data is missing or malformed. Destroying all sessions.', self::LOG_FILE_NAME );
348 $this->logout( $user_id );
349 return;
350 }
351 $user_ips = $this->get_user_ip();
352 // Exclude Loopback case. If condition returns true, then there is no Loopback intersection.
353 $common_ips = array_intersect( $user_ips, $this->get_localhost_ips() );
354 if ( array() !== $common_ips ) {
355 return;
356 }
357 // Work with the Session Lock options.
358 if ( $this->settings->is_property_locked( self::LOCK_IP_ADDRESS ) && isset( $session_data['defender_ip'] ) ) {
359 $ips = implode( '|', $user_ips );
360 $this->is_ip_allowed( $ips, $session_data['defender_ip'], $user_id );
361 }
362
363 if ( $this->settings->is_property_locked( self::LOCK_HOSTNAME ) && isset( $session_data['defender_hostname'] ) ) {
364 $hostname = defender_get_domain();
365 $this->is_hostname_allowed( $hostname, $session_data['defender_hostname'], $user_id );
366 }
367
368 if ( $this->settings->is_property_locked( self::LOCK_BROWSER ) && isset( $session_data['defender_browser'] ) ) {
369 $browser = defender_get_user_agent();
370 $this->is_browser_allowed( $browser, $session_data['defender_browser'], $user_id );
371 }
372 }
373
374 /**
375 * Verifies if the given IP is allowed based on the session information.
376 *
377 * @param string $ip IP addresses to check.
378 * @param string $session_ip Session IP address.
379 * @param int $user_id User ID.
380 *
381 * @return void
382 */
383 private function is_ip_allowed( $ip, $session_ip, $user_id ) {
384 if ( '' === $ip || '' === $session_ip || $ip !== $session_ip ) {
385 $this->log(
386 sprintf(
387 'IP address mismatch detected. Detected: %s, Expected: %s.',
388 $ip,
389 $session_ip
390 ),
391 self::LOG_FILE_NAME
392 );
393 /**
394 * Fires during session lock.
395 *
396 * @param int $user_id
397 * @param string $session_lock_type
398 */
399 do_action( 'wpdef_session_lock', $user_id, 'IP address' );
400 $this->logout( $user_id );
401 }
402 }
403
404 /**
405 * Verifies if the given hostname is allowed based on the session information.
406 *
407 * @param string $hostname Hostname to check.
408 * @param string $session_hostname Session hostname.
409 * @param int $user_id User ID.
410 *
411 * @return void
412 */
413 private function is_hostname_allowed( $hostname, $session_hostname, $user_id ) {
414 if ( '' === $hostname || '' === $session_hostname || strcasecmp( $hostname, $session_hostname ) !== 0 ) {
415 $this->log(
416 sprintf(
417 'Hostname mismatch detected. Detected: %s, Expected: %s.',
418 $hostname,
419 $session_hostname
420 ),
421 self::LOG_FILE_NAME
422 );
423 do_action( 'wpdef_session_lock', $user_id, 'hostname' );
424 $this->logout( $user_id );
425 }
426 }
427
428 /**
429 * Verifies if the given browser is allowed based on the session information.
430 *
431 * @param string $browser Browser to check.
432 * @param string $session_browser Session browser.
433 * @param int $user_id User ID.
434 *
435 * @return void
436 */
437 private function is_browser_allowed( $browser, $session_browser, $user_id ) {
438 if ( '' === $browser || '' === $session_browser || strcasecmp( $browser, $session_browser ) !== 0 ) {
439 $this->log(
440 sprintf(
441 'Browser mismatch detected. Detected: %s, Expected: %s.',
442 $browser,
443 $session_browser
444 ),
445 self::LOG_FILE_NAME
446 );
447 do_action( 'wpdef_session_lock', $user_id, 'browser' );
448 $this->logout( $user_id );
449 }
450 }
451
452 /**
453 * Should we apply this feature for the current user?
454 *
455 * @return bool
456 */
457 public function can_apply_session_protection(): bool {
458 $current_user = $this->get_user( get_current_user_id() );
459
460 /**
461 * Filter whether to apply the feature for the current user.
462 *
463 * @param bool $apply Whether to apply the feature.
464 * @param \WP_User $current_user The current user object.
465 *
466 * @return bool
467 */
468 $protection = apply_filters( 'wpdef_toggle_session_protection', $this->should_enforce_for_user( $current_user, $this->settings ), $current_user );
469
470 return is_bool( $protection ) ? $protection : (bool) $protection;
471 }
472 }
473