Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check haveibeenpwned API during password reset and account creation #170

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions includes/classes/Authentication/Passwords.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class Passwords {

use Singleton;

/**
* Stores the Have I Been Pwned API URL
*/
const HIBP_API_URL = 'https://api.pwnedpasswords.com/range/';
const HIBP_CACHE_KEY = 'tenup_experience_hibp';

/**
* Setup hooks
*
Expand Down Expand Up @@ -307,6 +313,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', __( '<strong>ERROR:</strong> The password entered may have been included in a data breach and is not considered safe to use. Please choose another.', 'tenup' ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought (non-blocking): Do we want to trigger an error here, or should we add a warning to the screen pre-update? Alternatively, should there be a checkbox, like the weak password box, that lets them bypass this if they really want to use that password?

Copy link
Member Author

@jamesmorrison jamesmorrison Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "tell, don't ask" and force an error here; as you've noted elsewhere I'll add a filter to disable the functionality.

}

// Should a strong password be enforced for this user?
if ( $user_id ) {

Expand Down Expand Up @@ -374,4 +385,66 @@ 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 {
jamesmorrison marked this conversation as resolved.
Show resolved Hide resolved
// 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 );

$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.
// 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;
}

$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 ) {
$is_password_secure = false;
}
}

// 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;
}
}
Loading