Skip to content

Commit ed335dd

Browse files
authored
Merge pull request #170 from 10up/feature/77-add-haveibeenpwned-check-during-password-validation
Check haveibeenpwned API during password reset and account creation
2 parents 265830c + f39c3b8 commit ed335dd

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ By default, all users must use a medium or greater strength password. This can b
8282

8383
**Password strength functionality requires the PHP extension [mbstring](https://www.php.net/manual/en/mbstring.installation.php) to be installed on the web server. Functionality will be bypassed if extension not installed.*
8484

85+
Additionally, the plugin checks passwords against the [Have I Been Pwned](https://haveibeenpwned.com/) database to ensure they haven't been compromised in a data breach. This can be disabled by defining the constant `TENUP_EXPERIENCE_DISABLE_HIBP` as `true`.
86+
87+
#### Constants
88+
89+
- `TENUP_EXPERIENCE_DISABLE_HIBP`
90+
91+
Define `TENUP_EXPERIENCE_DISABLE_HIBP` as `true` to disable Have I Been Pwned password checking.
92+
8593

8694
### Headers
8795

includes/classes/Authentication/Passwords.php

+73
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ class Passwords {
1717

1818
use Singleton;
1919

20+
/**
21+
* Stores the Have I Been Pwned API URL
22+
*/
23+
const HIBP_API_URL = 'https://api.pwnedpasswords.com/range/';
24+
const HIBP_CACHE_KEY = 'tenup_experience_hibp';
25+
2026
/**
2127
* Setup hooks
2228
*
@@ -307,6 +313,11 @@ public function validate_strong_password( $errors, $user_data ) {
307313
return $errors;
308314
}
309315

316+
// Validate the password against the Have I Been Pwned API.
317+
if ( ! $this->is_password_secure( $password ) && is_wp_error( $errors ) ) {
318+
$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' ) );
319+
}
320+
310321
// Should a strong password be enforced for this user?
311322
if ( $user_id ) {
312323

@@ -374,4 +385,66 @@ public function enforce_for_user( $user_id ) {
374385

375386
return $enforce;
376387
}
388+
389+
/**
390+
* Check if password is secure by querying the Have I Been Pwned API.
391+
*
392+
* @param string $password Password to validate.
393+
*
394+
* @return bool True if password is ok, false if it shows up in a breach.
395+
*/
396+
protected function is_password_secure( $password ): bool {
397+
// Default
398+
$is_password_secure = true;
399+
400+
// Allow opt-out of Have I Been Pwned check through a constant or filter.
401+
if (
402+
( defined( 'TENUP_EXPERIENCE_DISABLE_HIBP' ) && TENUP_EXPERIENCE_DISABLE_HIBP ) ||
403+
apply_filters( 'tenup_experience_disable_hibp', false, $password )
404+
) {
405+
return true;
406+
}
407+
408+
$hash = strtoupper( sha1( $password ) );
409+
$prefix = substr( $hash, 0, 5 );
410+
$suffix = substr( $hash, 5 );
411+
412+
$cached_result = wp_cache_get( $prefix . $suffix, self::HIBP_CACHE_KEY );
413+
414+
if ( false !== $cached_result || false ) { // remove || false; only used for testing
415+
return $cached_result;
416+
}
417+
418+
$response = wp_remote_get( self::HIBP_API_URL . $prefix, [ 'user-agent' => '10up Experience WordPress Plugin' ] );
419+
420+
// Allow for a failed request to the HIPB API.
421+
// Don't cache the result if the request failed.
422+
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
423+
return true;
424+
}
425+
426+
$body = wp_remote_retrieve_body( $response );
427+
428+
// Allow for a failed request to the HIPB API.
429+
// Don't cache the result if the request failed.
430+
if ( is_wp_error( $body ) ) {
431+
return true;
432+
}
433+
434+
$lines = explode( "\r\n", $body );
435+
436+
foreach ( $lines as $line ) {
437+
$parts = explode( ':', $line );
438+
439+
// If the suffix is found in the response, the password may be in a breach.
440+
if ( $parts[0] === $suffix ) {
441+
$is_password_secure = false;
442+
}
443+
}
444+
445+
// Cache the result for 4 hours.
446+
wp_cache_set( $prefix . $suffix, (int) $is_password_secure, self::HIBP_CACHE_KEY, 60 * 60 * 4 );
447+
448+
return $is_password_secure;
449+
}
377450
}

0 commit comments

Comments
 (0)