403 ] ); } /* --------------------------------------------------------------------- */ /* IP blocklist */ /* --------------------------------------------------------------------- */ public function enforce_ip_blocklist() { if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return; } if ( ! Helper::get_setting( 'firewall_enabled', true ) ) { return; } $blocklist = (array) Helper::get_setting( 'ip_blocklist', [] ); if ( empty( $blocklist ) ) { return; } // Never lock out an already-authenticated administrator. if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) { return; } $ip = Helper::get_ip_address(); if ( '0.0.0.0' === $ip ) { return; } // Safelisted addresses are never blocked, even when logged out. if ( \GemSecurity\Security\SiteGuard::ip_safelisted( $ip ) ) { return; } foreach ( $blocklist as $entry ) { if ( self::ip_matches( $ip, trim( (string) $entry ) ) ) { Logger::log( 'firewall_block', sprintf( 'Blocked request from blocklisted IP %s.', $ip ), Logger::SEVERITY_WARNING, [ 'user_id' => 0, 'ip_address' => $ip ] ); status_header( 403 ); nocache_headers(); wp_die( esc_html__( 'Your access to this site has been blocked.', 'gemsecurity' ), esc_html__( 'Access denied', 'gemsecurity' ), [ 'response' => 403 ] ); } } } /** * Match an IP against an exact address or CIDR range (IPv4 + IPv6). * * @param string $ip * @param string $entry * @return bool */ public static function ip_matches( $ip, $entry ) { if ( '' === $entry ) { return false; } if ( false === strpos( $entry, '/' ) ) { return $ip === $entry; } list( $subnet, $bits ) = explode( '/', $entry, 2 ); $bits = (int) $bits; $ip_bin = @inet_pton( $ip ); $subnet_bin = @inet_pton( $subnet ); if ( false === $ip_bin || false === $subnet_bin || strlen( $ip_bin ) !== strlen( $subnet_bin ) ) { return false; } $bytes = intdiv( $bits, 8 ); $rem = $bits % 8; if ( $bytes > 0 && 0 !== substr_compare( $ip_bin, $subnet_bin, 0, $bytes ) ) { return false; } if ( 0 === $rem ) { return true; } $mask = chr( 0xff << ( 8 - $rem ) & 0xff ); return ( $ip_bin[ $bytes ] & $mask ) === ( $subnet_bin[ $bytes ] & $mask ); } /* --------------------------------------------------------------------- */ /* Hardening */ /* --------------------------------------------------------------------- */ public function apply_hardening() { // Disable the theme / plugin file editors. if ( Helper::get_setting( 'disable_file_editing', true ) && ! defined( 'DISALLOW_FILE_EDIT' ) ) { define( 'DISALLOW_FILE_EDIT', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core constant. } // Block ?author=N enumeration for anonymous visitors. if ( Helper::get_setting( 'block_author_enumeration', true ) && ! is_admin() && ! is_user_logged_in() ) { // Blocks ?author=N enumeration on anonymous requests; it IS the // request we are deciding to serve, so no nonce could exist. $author = isset( $_GET['author'] ) ? sanitize_text_field( wp_unslash( $_GET['author'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' !== $author && preg_match( '/^\d+$/', $author ) ) { Logger::log( 'author_enumeration_blocked', sprintf( 'Blocked author enumeration attempt (?author=%s).', $author ), Logger::SEVERITY_NOTICE, [ 'user_id' => 0 ] ); wp_safe_redirect( home_url(), 301 ); exit; } } } /** * Remove the /wp/v2/users endpoints for unauthenticated requests so the * REST API cannot be used to enumerate usernames. * * @param array $endpoints * @return array */ public function maybe_remove_users_endpoint( $endpoints ) { if ( is_user_logged_in() ) { return $endpoints; } foreach ( [ '/wp/v2/users', '/wp/v2/users/(?P[\d]+)' ] as $route ) { if ( isset( $endpoints[ $route ] ) ) { unset( $endpoints[ $route ] ); } } return $endpoints; } /** * Replace WordPress's specific login failure text ("unknown username", * "the password you entered … is incorrect") with one message that reveals * nothing about which half was wrong. * * @return string */ public function generic_login_error() { return __( 'The username or password you entered is incorrect.', 'gemsecurity' ); } }