| @@ -146,16 +146,49 @@ | ||
| 146 | 146 | $this->update_last_activity(); |
| 147 | 147 | } |
| 148 | 148 | |
| 149 | 149 | /** |
| 150 | + * Get transient key for server-side session last activity. | |
| 151 | + * | |
| 152 | + * @param int|null $user_id User ID. | |
| 153 | + * @return string|null | |
| 154 | + */ | |
| 155 | + private function get_session_activity_key( $user_id = null ): ?string { | |
| 156 | + $user_id = $user_id ?? get_current_user_id(); | |
| 157 | + if ( ! $user_id ) { | |
| 158 | + return null; | |
| 159 | + } | |
| 160 | + | |
| 161 | + $token = wp_get_session_token(); | |
| 162 | + if ( ! $token ) { | |
| 163 | + return 'wpdef_last_act_' . $user_id; | |
| 164 | + } | |
| 165 | + | |
| 166 | + return 'wpdef_last_act_' . $user_id . '_' . substr( md5( $token ), 0, 16 ); | |
| 167 | + } | |
| 168 | + | |
| 169 | + /** | |
| 150 | 170 | * Retrieves the last activity timestamp. |
| 151 | 171 | * |
| 152 | 172 | * @return int|null Last activity timestamp or null if not set. |
| 153 | 173 | */ |
| 154 | 174 | private function get_last_activity() { |
| 155 | - // Check for client-side cookie first to avoid unnecessary database queries. | |
| 175 | + // First check server-side session state if available to prevent client tampering. | |
| 176 | + $key = $this->get_session_activity_key(); | |
| 177 | + if ( $key ) { | |
| 178 | + $server_activity = get_transient( $key ); | |
| 179 | + if ( is_numeric( $server_activity ) && (int) $server_activity > 0 ) { | |
| 180 | + return (int) $server_activity; | |
| 181 | + } | |
| 182 | + } | |
| 183 | + | |
| 184 | + // Fallback to client-side cookie if server transient is missing. | |
| 156 | 185 | if ( isset( $_COOKIE['wpdef_last_activity'] ) && is_numeric( $_COOKIE['wpdef_last_activity'] ) ) { |
| 157 | - return (int) $_COOKIE['wpdef_last_activity']; | |
| 186 | + $timestamp = (int) $_COOKIE['wpdef_last_activity']; | |
| 187 | + $now = time(); | |
| 188 | + | |
| 189 | + // Clamp future timestamps to current server time to prevent session timeout bypass. | |
| 190 | + return min( $timestamp, $now ); | |
| 158 | 191 | } |
| 159 | 192 | |
| 160 | 193 | return null; |
| 161 | 194 | } |
| @@ -169,9 +202,9 @@ | ||
| 169 | 202 | */ |
| 170 | 203 | public function logout( $user_id = null ) { |
| 171 | 204 | if ( is_user_logged_in() ) { |
| 172 | 205 | // Case without $user_id may be from Ajax. |
| 173 | - if ( empty( $user_id ) ) { | |
| 206 | + if ( is_null( $user_id ) ) { | |
| 174 | 207 | $user_id = get_current_user_id(); |
| 175 | 208 | $this->log( |
| 176 | 209 | sprintf( 'User session timed out due to inactivity during %s hours', $this->settings->idle_timeout ), |
| 177 | 210 | self::LOG_FILE_NAME |
| @@ -178,8 +211,14 @@ | ||
| 178 | 211 | ); |
| 179 | 212 | // Fires during session timeout. |
| 180 | 213 | do_action( 'wpdef_session_timeout', $user_id ); |
| 181 | 214 | } |
| 215 | + | |
| 216 | + $key = $this->get_session_activity_key( $user_id ); | |
| 217 | + if ( $key ) { | |
| 218 | + delete_transient( $key ); | |
| 219 | + } | |
| 220 | + | |
| 182 | 221 | // Get current session token. |
| 183 | 222 | $current_session_token = wp_get_session_token(); |
| 184 | 223 | if ( $current_session_token ) { |
| 185 | 224 | // Get session manager for the user. |
| @@ -210,12 +249,10 @@ | ||
| 210 | 249 | * |
| 211 | 250 | * @return \WP_Error |
| 212 | 251 | */ |
| 213 | 252 | public function login_modal_message( $errors ) { |
| 214 | - if ( | |
| 215 | - empty( defender_get_data_from_request( 'interim-login', 'g' ) ) || | |
| 216 | - ! get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) | |
| 217 | - ) { | |
| 253 | + $interim_login = defender_get_data_from_request( 'interim-login', 'g' ); | |
| 254 | + if ( is_null( $interim_login ) || '' === $interim_login || 1 !== get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) ) { | |
| 218 | 255 | return $errors; |
| 219 | 256 | } |
| 220 | 257 | |
| 221 | 258 | delete_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ); |
| @@ -246,9 +283,9 @@ | ||
| 246 | 283 | * |
| 247 | 284 | * @return void |
| 248 | 285 | */ |
| 249 | 286 | public function login_modal_message_styles(): void { |
| 250 | - if ( get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) ) { | |
| 287 | + if ( 1 === get_site_transient( self::LOGOUT_MSG_TRANSIENT_KEY ) ) { | |
| 251 | 288 | // Don't delete the transient here, it will be deleted in the login_modal_message() method. |
| 252 | 289 | |
| 253 | 290 | echo '<style> |
| 254 | 291 | #login_error { |
| @@ -292,9 +329,10 @@ | ||
| 292 | 329 | * @param int $idle_timeout The idle timeout duration in seconds. |
| 293 | 330 | * |
| 294 | 331 | * @return int The idle timeout duration in seconds. |
| 295 | 332 | */ |
| 296 | - return (int) apply_filters( 'wpdef_idle_timeout', $this->settings->idle_timeout * HOUR_IN_SECONDS ); | |
| 333 | + $idle_timeout = apply_filters( 'wpdef_idle_timeout', $this->settings->idle_timeout * HOUR_IN_SECONDS ); | |
| 334 | + return ! is_int( $idle_timeout ) ? (int) $idle_timeout : $idle_timeout; | |
| 297 | 335 | } |
| 298 | 336 | |
| 299 | 337 | /** |
| 300 | 338 | * Updates the last activity timestamp efficiently. |
| @@ -301,13 +339,19 @@ | ||
| 301 | 339 | * |
| 302 | 340 | * @return void |
| 303 | 341 | */ |
| 304 | 342 | public function update_last_activity() { |
| 305 | - $current_time = time(); | |
| 343 | + $current_time = time(); | |
| 344 | + | |
| 345 | + $key = $this->get_session_activity_key(); | |
| 346 | + if ( $key ) { | |
| 347 | + set_transient( $key, $current_time, DAY_IN_SECONDS ); | |
| 348 | + } | |
| 349 | + | |
| 306 | 350 | $last_activity = $this->get_last_activity(); |
| 307 | 351 | |
| 308 | 352 | // Only update the cookie if minutes have passed since the last update. |
| 309 | - if ( ! empty( $last_activity ) || ( $current_time - $last_activity > MINUTE_IN_SECONDS ) ) { | |
| 353 | + if ( ! is_null( $last_activity ) || ( $current_time - $last_activity > MINUTE_IN_SECONDS ) ) { | |
| 310 | 354 | setcookie( 'wpdef_last_activity', $current_time, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); |
| 311 | 355 | } |
| 312 | 356 | } |
| 313 | 357 | |
| @@ -350,9 +394,10 @@ | ||
| 350 | 394 | return; |
| 351 | 395 | } |
| 352 | 396 | $user_ips = $this->get_user_ip(); |
| 353 | 397 | // Exclude Loopback case. If condition returns true, then there is no Loopback intersection. |
| 354 | - if ( ! empty( array_intersect( $user_ips, $this->get_localhost_ips() ) ) ) { | |
| 398 | + $common_ips = array_intersect( $user_ips, $this->get_localhost_ips() ); | |
| 399 | + if ( array() !== $common_ips ) { | |
| 355 | 400 | return; |
| 356 | 401 | } |
| 357 | 402 | // Work with the Session Lock options. |
| 358 | 403 | if ( $this->settings->is_property_locked( self::LOCK_IP_ADDRESS ) && isset( $session_data['defender_ip'] ) ) { |
| @@ -380,9 +425,9 @@ | ||
| 380 | 425 | * |
| 381 | 426 | * @return void |
| 382 | 427 | */ |
| 383 | 428 | private function is_ip_allowed( $ip, $session_ip, $user_id ) { |
| 384 | - if ( empty( $ip ) || empty( $session_ip ) || $ip !== $session_ip ) { | |
| 429 | + if ( '' === $ip || '' === $session_ip || $ip !== $session_ip ) { | |
| 385 | 430 | $this->log( |
| 386 | 431 | sprintf( |
| 387 | 432 | 'IP address mismatch detected. Detected: %s, Expected: %s.', |
| 388 | 433 | $ip, |
| @@ -410,9 +455,9 @@ | ||
| 410 | 455 | * |
| 411 | 456 | * @return void |
| 412 | 457 | */ |
| 413 | 458 | private function is_hostname_allowed( $hostname, $session_hostname, $user_id ) { |
| 414 | - if ( empty( $hostname ) || empty( $session_hostname ) || strcasecmp( $hostname, $session_hostname ) !== 0 ) { | |
| 459 | + if ( '' === $hostname || '' === $session_hostname || strcasecmp( $hostname, $session_hostname ) !== 0 ) { | |
| 415 | 460 | $this->log( |
| 416 | 461 | sprintf( |
| 417 | 462 | 'Hostname mismatch detected. Detected: %s, Expected: %s.', |
| 418 | 463 | $hostname, |
| @@ -434,9 +479,9 @@ | ||
| 434 | 479 | * |
| 435 | 480 | * @return void |
| 436 | 481 | */ |
| 437 | 482 | private function is_browser_allowed( $browser, $session_browser, $user_id ) { |
| 438 | - if ( empty( $browser ) || empty( $session_browser ) || strcasecmp( $browser, $session_browser ) !== 0 ) { | |
| 483 | + if ( '' === $browser || '' === $session_browser || strcasecmp( $browser, $session_browser ) !== 0 ) { | |
| 439 | 484 | $this->log( |
| 440 | 485 | sprintf( |
| 441 | 486 | 'Browser mismatch detected. Detected: %s, Expected: %s.', |
| 442 | 487 | $browser, |
| @@ -464,7 +509,9 @@ | ||
| 464 | 509 | * @param \WP_User $current_user The current user object. |
| 465 | 510 | * |
| 466 | 511 | * @return bool |
| 467 | 512 | */ |
| 468 | - return (bool) apply_filters( 'wpdef_toggle_session_protection', $this->should_enforce_for_user( $current_user ), $current_user ); | |
| 513 | + $protection = apply_filters( 'wpdef_toggle_session_protection', $this->should_enforce_for_user( $current_user, $this->settings ), $current_user ); | |
| 514 | + | |
| 515 | + return is_bool( $protection ) ? $protection : (bool) $protection; | |
| 469 | 516 | } |
| 470 | 517 | } |