| @@ -14,9 +14,9 @@ | ||
| 14 | 14 | use WP_Defender\Model\Lockout_Ip; |
| 15 | 15 | use WP_Defender\Behavior\WPMUDEV; |
| 16 | 16 | use WP_Defender\Model\Lockout_Log; |
| 17 | 17 | use WP_Defender\Model\Onboard; |
| 18 | -use MaxMind\Db\Reader\InvalidDatabaseException; | |
| 18 | +use WP_DEFENDER_VENDOR\MaxMind\Db\Reader\InvalidDatabaseException; | |
| 19 | 19 | use WP_Defender\Model\Setting\Firewall as Model_Firewall; |
| 20 | 20 | use WP_Defender\Component\Trusted_Proxy_Preset\Cloudflare_Proxy; |
| 21 | 21 | use WP_Defender\Component\Trusted_Proxy_Preset\Trusted_Proxy_Preset; |
| 22 | 22 | use WP_Defender\Controller\Firewall as Firewall_Controller; |
| @@ -63,18 +63,50 @@ | ||
| 63 | 63 | private function is_commencing_staff_access( $access ): bool { |
| 64 | 64 | $action = defender_get_data_from_request( 'action', 'g' ); |
| 65 | 65 | $wdpunkey = defender_get_data_from_request( 'wdpunkey', 'p' ); |
| 66 | 66 | |
| 67 | - return wp_doing_ajax() && 'wdpunauth' === $action && hash_equals( $wdpunkey, $access['key'] ); | |
| 67 | + return wp_doing_ajax() && 'wdpunauth' === $action && isset( $access['key'] ) && hash_equals( $wdpunkey, $access['key'] ); | |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | 70 | /** |
| 71 | 71 | * Check is the access from authenticated staff. |
| 72 | 72 | * |
| 73 | - * @return bool | |
| 73 | + * Validates the `wpmudev_is_staff` cookie token by verifying: | |
| 74 | + * - Token version and structure (v1.<payload>.<signature>) | |
| 75 | + * - HMAC-SHA256 signature against site salt (`wpmudev_remote_access`) | |
| 76 | + * - Token expiration timestamp against current time | |
| 77 | + * - Audience (`aud`) against current site URL (`home_url('/')`) | |
| 78 | + * - Subject (`sub`) against currently authenticated user ID | |
| 79 | + * | |
| 80 | + * @return bool True if staff access token is valid and unexpired, false otherwise. | |
| 74 | 81 | */ |
| 75 | 82 | private function is_authenticated_staff_access(): bool { |
| 76 | - return '1' === defender_get_data_from_request( 'wpmudev_is_staff', 'c' ); | |
| 83 | + $cookie = defender_get_data_from_request( 'wpmudev_is_staff', 'c' ); | |
| 84 | + if ( ! is_string( $cookie ) ) { | |
| 85 | + return false; | |
| 86 | + } | |
| 87 | + | |
| 88 | + $token = explode( '.', $cookie, 3 ); | |
| 89 | + if ( 3 !== count( $token ) || 'v1' !== $token[0] ) { | |
| 90 | + return false; | |
| 91 | + } | |
| 92 | + | |
| 93 | + $signature = hash_hmac( 'sha256', $token[1], wp_salt( 'wpmudev_remote_access' ) ); | |
| 94 | + if ( ! hash_equals( $signature, $token[2] ) ) { | |
| 95 | + return false; | |
| 96 | + } | |
| 97 | + // Improve flow to avoid bypassing staff access authentication via an unsigned cookie. Use legitim payload. | |
| 98 | + $payload = base64_decode( strtr( $token[1], '-_', '+/' ) . str_repeat( '=', ( 4 - strlen( $token[1] ) % 4 ) % 4 ), true );// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 99 | + $payload = is_string( $payload ) ? json_decode( $payload, true ) : null; | |
| 100 | + | |
| 101 | + return is_array( $payload ) && | |
| 102 | + isset( $payload['aud'], $payload['exp'], $payload['sub'] ) && | |
| 103 | + is_string( $payload['aud'] ) && | |
| 104 | + is_numeric( $payload['exp'] ) && | |
| 105 | + is_numeric( $payload['sub'] ) && | |
| 106 | + (int) $payload['exp'] > time() && | |
| 107 | + hash_equals( home_url( '/' ), $payload['aud'] ) && | |
| 108 | + get_current_user_id() === (int) $payload['sub']; | |
| 77 | 109 | } |
| 78 | 110 | |
| 79 | 111 | /** |
| 80 | 112 | * Check if the access is from our staff access. |
| @@ -85,15 +117,24 @@ | ||
| 85 | 117 | if ( defined( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) && true === constant( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) ) { |
| 86 | 118 | return false; |
| 87 | 119 | } |
| 88 | 120 | |
| 89 | - $wpmu_dev = new WPMUDEV(); | |
| 90 | - $is_remote_access = $wpmu_dev->get_apikey() && | |
| 121 | + $wpmu_dev = new WPMUDEV(); | |
| 122 | + | |
| 123 | + // Remote staff access needs Dashboard mode. | |
| 124 | + if ( 'dashboard' !== $wpmu_dev->resolve_connection_mode() ) { | |
| 125 | + return false; | |
| 126 | + } | |
| 127 | + | |
| 128 | + $is_remote_access = is_object( WPMUDEV_Dashboard::$api ) && | |
| 91 | 129 | true === WPMUDEV_Dashboard::$api->remote_access_details( 'enabled' ); |
| 92 | 130 | |
| 93 | 131 | if ( $is_remote_access ) { |
| 94 | - $access = $wpmu_dev->get_remote_access(); | |
| 95 | - if ( $this->is_authenticated_staff_access() || $this->is_commencing_staff_access( $access ) ) { | |
| 132 | + $access = $wpmu_dev->get_remote_access(); | |
| 133 | + $is_authenticated_staff_access = isset( $access['userid'] ) && | |
| 134 | + get_current_user_id() === (int) $access['userid'] && | |
| 135 | + $this->is_authenticated_staff_access(); | |
| 136 | + if ( $is_authenticated_staff_access || $this->is_commencing_staff_access( $access ) ) { | |
| 96 | 137 | $this->log( $access, Firewall_Controller::FIREWALL_LOG ); |
| 97 | 138 | |
| 98 | 139 | return true; |
| 99 | 140 | } |
| @@ -114,9 +155,12 @@ | ||
| 114 | 155 | return; |
| 115 | 156 | } |
| 116 | 157 | $time_string = '-' . $storage_days . ' days'; |
| 117 | 158 | $timestamp = $this->local_to_utc( $time_string ); |
| 118 | - Lockout_Log::remove_logs( $timestamp, 50 ); | |
| 159 | + // Since v5.7.0. | |
| 160 | + $delete_count = apply_filters( 'wpdef_firewall_limit_deleted_logs', 50 ); | |
| 161 | + $delete_count = is_int( $delete_count ) ? $delete_count : (int) $delete_count; | |
| 162 | + Lockout_Log::remove_logs( $timestamp, $delete_count ); | |
| 119 | 163 | } |
| 120 | 164 | |
| 121 | 165 | /** |
| 122 | 166 | * Cron for clean up temporary IP block list. |
| @@ -269,9 +313,10 @@ | ||
| 269 | 313 | * @return int |
| 270 | 314 | * @since 3.7.0 Get the limit of Lockout records. |
| 271 | 315 | */ |
| 272 | 316 | public function get_lockout_record_limit() { |
| 273 | - return (int) apply_filters( 'wd_lockout_record_limit', 10000 ); | |
| 317 | + $limit = apply_filters( 'wd_lockout_record_limit', 10000 ); | |
| 318 | + return is_int( $limit ) ? $limit : (int) $limit; | |
| 274 | 319 | } |
| 275 | 320 | |
| 276 | 321 | /** |
| 277 | 322 | * Cron for deleting unwanted lockout records. |
| @@ -325,9 +370,9 @@ | ||
| 325 | 370 | |
| 326 | 371 | $gathered_ips = array(); |
| 327 | 372 | $server = defender_get_data_from_request( null, 's' ); |
| 328 | 373 | foreach ( $ip_headers as $header ) { |
| 329 | - if ( ! empty( $server[ $header ] ) ) { | |
| 374 | + if ( isset( $server[ $header ] ) && '' !== $server[ $header ] ) { | |
| 330 | 375 | // Handle multiple IP addresses. |
| 331 | 376 | $ips = array_map( 'trim', explode( ',', $server[ $header ] ) ); |
| 332 | 377 | |
| 333 | 378 | foreach ( $ips as $ip ) { |
| @@ -345,9 +390,9 @@ | ||
| 345 | 390 | * |
| 346 | 391 | * @since 4.5.1 |
| 347 | 392 | * @deprecated 5.1.0 No longer used and will be removed in a future version. |
| 348 | 393 | */ |
| 349 | - $gathered_ips = (array) apply_filters_deprecated( | |
| 394 | + $gathered_ips = apply_filters_deprecated( | |
| 350 | 395 | 'wpdef_firewall_gathered_ips', |
| 351 | 396 | array_unique( $gathered_ips ), |
| 352 | 397 | '5.1.0', |
| 353 | 398 | '', |
| @@ -352,8 +397,9 @@ | ||
| 352 | 397 | '5.1.0', |
| 353 | 398 | '', |
| 354 | 399 | 'This filter will be removed in a future version. If you are using it, update your implementation.' |
| 355 | 400 | ); |
| 401 | + $gathered_ips = ! is_array( $gathered_ips ) ? (array) $gathered_ips : $gathered_ips; | |
| 356 | 402 | |
| 357 | 403 | return $this->filter_user_ips( $gathered_ips ); |
| 358 | 404 | } |
| 359 | 405 | |
| @@ -409,9 +455,9 @@ | ||
| 409 | 455 | * @return void |
| 410 | 456 | */ |
| 411 | 457 | public function update_trusted_proxy_preset_ips(): void { |
| 412 | 458 | $model = wd_di()->get( Model_Firewall::class ); |
| 413 | - if ( ! empty( $model->trusted_proxy_preset ) ) { | |
| 459 | + if ( '' !== $model->trusted_proxy_preset ) { | |
| 414 | 460 | /** |
| 415 | 461 | * Retrieve Trusted_Proxy_Preset instance. |
| 416 | 462 | * |
| 417 | 463 | * @var Trusted_Proxy_Preset $trusted_proxy_preset |
| @@ -570,9 +616,10 @@ | ||
| 570 | 616 | * @param bool $enable True to enable whitelist server public IP, false otherwise. |
| 571 | 617 | * |
| 572 | 618 | * @since 5.0.2 |
| 573 | 619 | */ |
| 574 | - return (bool) apply_filters( 'wpdef_firewall_whitelist_server_public_ip_enabled', true ); | |
| 620 | + $enabled = apply_filters( 'wpdef_firewall_whitelist_server_public_ip_enabled', true ); | |
| 621 | + return is_bool( $enabled ) ? $enabled : (bool) $enabled; | |
| 575 | 622 | } |
| 576 | 623 | |
| 577 | 624 | /** |
| 578 | 625 | * Set whitelist server public IP. |
| @@ -585,9 +632,9 @@ | ||
| 585 | 632 | return false; |
| 586 | 633 | } |
| 587 | 634 | |
| 588 | 635 | $ip = wd_di()->get( Smart_Ip_Detection::class )->get_server_public_ip(); |
| 589 | - if ( empty( $ip ) ) { | |
| 636 | + if ( '' === $ip ) { | |
| 590 | 637 | $this->log( 'Failed to whitelist server public IP.', Firewall_Controller::FIREWALL_LOG ); |
| 591 | 638 | return false; |
| 592 | 639 | } |
| 593 | 640 | |