From 28190b8d690a7bdd62e60bcc5c57273df32f7381 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 10 Dec 2024 13:12:55 +0000 Subject: [PATCH 1/2] Added check against HIBP API when setting a password. --- includes/classes/Authentication/Passwords.php | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/includes/classes/Authentication/Passwords.php b/includes/classes/Authentication/Passwords.php index 9e43702..21ba2a5 100644 --- a/includes/classes/Authentication/Passwords.php +++ b/includes/classes/Authentication/Passwords.php @@ -17,6 +17,11 @@ class Passwords { use Singleton; + /** + * Stores the Have I Been Pwned API URL + */ + const API_URL = 'https://api.pwnedpasswords.com/range/'; + /** * Setup hooks * @@ -307,6 +312,11 @@ public function validate_strong_password( $errors, $user_data ) { return $errors; } + // Validate the password against the Have I Been Pwned API. + if ( ! $this->is_password_secure( $password ) && is_wp_error( $errors ) ) { + $errors->add( 'password_reset_error', __( 'ERROR: The password entered may have been included in a data breach and is not considered safe to use. Please choose another.', 'tenup' ) ); + } + // Should a strong password be enforced for this user? if ( $user_id ) { @@ -374,4 +384,44 @@ public function enforce_for_user( $user_id ) { return $enforce; } + + /** + * Check if password is secure by querying the Have I Been Pwned API. + * + * @param string $password Password to validate. + * + * @return bool True if password is ok, false if it shows up in a breach. + */ + protected function is_password_secure( $password ): bool { + $hash = strtoupper( sha1( $password ) ); + $prefix = substr( $hash, 0, 5 ); + $suffix = substr( $hash, 5 ); + + $response = wp_remote_get( self::API_URL . $prefix ); + + // Allow for a failed request to the HIPB API. + if ( is_wp_error( $response ) ) { + return true; + } + + $body = wp_remote_retrieve_body( $response ); + + // Allow for a failed request to the HIPB API. + if ( is_wp_error( $body ) ) { + return true; + } + + $lines = explode( "\r\n", $body ); + + foreach ( $lines as $line ) { + $parts = explode( ':', $line ); + + // If the suffix is found in the response, the password may be in a breach. + if ( $parts[0] === $suffix ) { + return false; + } + } + + return true; + } } From 6e4b84154dfb98c2b78fb3ec8f10781493a72259 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 23 Jan 2025 16:26:50 +0000 Subject: [PATCH 2/2] PR feedback. --- includes/classes/Authentication/Passwords.php | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/includes/classes/Authentication/Passwords.php b/includes/classes/Authentication/Passwords.php index 21ba2a5..3fa76f1 100644 --- a/includes/classes/Authentication/Passwords.php +++ b/includes/classes/Authentication/Passwords.php @@ -20,7 +20,8 @@ class Passwords { /** * Stores the Have I Been Pwned API URL */ - const API_URL = 'https://api.pwnedpasswords.com/range/'; + const HIBP_API_URL = 'https://api.pwnedpasswords.com/range/'; + const HIBP_CACHE_KEY = 'tenup_experience_hibp'; /** * Setup hooks @@ -393,20 +394,39 @@ public function enforce_for_user( $user_id ) { * @return bool True if password is ok, false if it shows up in a breach. */ protected function is_password_secure( $password ): bool { + // Default + $is_password_secure = true; + + // Allow opt-out of Have I Been Pwned check through a constant or filter. + if ( + ( defined( 'TENUP_EXPERIENCE_DISABLE_HIBP' ) && TENUP_EXPERIENCE_DISABLE_HIBP ) || + apply_filters( 'tenup_experience_disable_hibp', false, $password ) + ) { + return true; + } + $hash = strtoupper( sha1( $password ) ); $prefix = substr( $hash, 0, 5 ); $suffix = substr( $hash, 5 ); - $response = wp_remote_get( self::API_URL . $prefix ); + $cached_result = wp_cache_get( $prefix . $suffix, self::HIBP_CACHE_KEY ); + + if ( false !== $cached_result || false ) { // remove || false; only used for testing + return $cached_result; + } + + $response = wp_remote_get( self::HIBP_API_URL . $prefix, [ 'user-agent' => '10up Experience WordPress Plugin' ] ); // Allow for a failed request to the HIPB API. - if ( is_wp_error( $response ) ) { + // Don't cache the result if the request failed. + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { return true; } $body = wp_remote_retrieve_body( $response ); // Allow for a failed request to the HIPB API. + // Don't cache the result if the request failed. if ( is_wp_error( $body ) ) { return true; } @@ -418,10 +438,13 @@ protected function is_password_secure( $password ): bool { // If the suffix is found in the response, the password may be in a breach. if ( $parts[0] === $suffix ) { - return false; + $is_password_secure = false; } } - return true; + // Cache the result for 4 hours. + wp_cache_set( $prefix . $suffix, (int) $is_password_secure, self::HIBP_CACHE_KEY, 60 * 60 * 4 ); + + return $is_password_secure; } }