check( $pw, $errors ); } /** * @param \WP_Error $errors * @param \WP_User|\WP_Error $user */ public function validate_reset( $errors, $user ) { // validate_password_reset is only called from inside WP's own reset // flow, which has already verified the reset-password nonce. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $pw = isset( $_POST['pass1'] ) ? (string) wp_unslash( $_POST['pass1'] ) : ''; if ( '' === $pw ) { return; } $this->check( $pw, $errors ); } /** * Enforce policy on the self-registration form (Settings > General > Anyone can register). * * @param \WP_Error $errors * @return \WP_Error */ public function validate_registration( $errors ) { // registration_errors runs inside WP's own self-register handler, // which has already verified its nonce; the raw password must stay // un-sanitized. // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $pw = isset( $_POST['user_pass'] ) ? (string) wp_unslash( $_POST['user_pass'] ) : ''; if ( '' === $pw ) { return $errors; } $this->check( $pw, $errors ); return $errors; } /** * Apply every enabled rule, adding messages to $errors. * * @param string $pw * @param \WP_Error $errors */ private function check( $pw, $errors ) { $min = max( 6, (int) Helper::get_setting( 'password_min_length', 10 ) ); if ( strlen( $pw ) < $min ) { /* translators: %d: minimum password length. */ $errors->add( 'gemsec_pw_short', sprintf( __( 'Error: Password must be at least %d characters.', 'gemsecurity' ), $min ) ); } if ( Helper::get_setting( 'password_require_complexity', true ) ) { $missing = []; if ( ! preg_match( '/[a-z]/', $pw ) ) { $missing[] = __( 'a lowercase letter', 'gemsecurity' ); } if ( ! preg_match( '/[A-Z]/', $pw ) ) { $missing[] = __( 'an uppercase letter', 'gemsecurity' ); } if ( ! preg_match( '/[0-9]/', $pw ) ) { $missing[] = __( 'a number', 'gemsecurity' ); } if ( ! preg_match( '/[^a-zA-Z0-9]/', $pw ) ) { $missing[] = __( 'a symbol', 'gemsecurity' ); } if ( $missing ) { /* translators: %s: comma-separated list of missing character classes (e.g. "a number, a symbol"). */ $errors->add( 'gemsec_pw_weak', sprintf( __( 'Error: Password must include %s.', 'gemsecurity' ), implode( ', ', $missing ) ) ); } } if ( Helper::get_setting( 'password_check_breached', true ) && $this->is_breached( $pw ) ) { $errors->add( 'gemsec_pw_breached', __( 'Error: This password has appeared in a known data breach. Please choose a different one.', 'gemsecurity' ) ); } } /** * Have I Been Pwned range query (k-anonymity). Fail-open on error. * * @param string $pw * @return bool */ private function is_breached( $pw ) { $hash = strtoupper( sha1( $pw ) ); $prefix = substr( $hash, 0, 5 ); $suffix = substr( $hash, 5 ); $response = wp_remote_get( 'https://api.pwnedpasswords.com/range/' . $prefix, [ 'timeout' => 5, 'headers' => [ 'Add-Padding' => 'true' ], ] ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { return false; } foreach ( preg_split( '/\r?\n/', wp_remote_retrieve_body( $response ) ) as $line ) { $parts = explode( ':', trim( $line ) ); if ( isset( $parts[0] ) && hash_equals( $suffix, strtoupper( $parts[0] ) ) ) { return (int) ( $parts[1] ?? 0 ) > 0; } } return false; } /* ----------------------------- Expiry ----------------------------- */ public function stamp( $user ) { if ( $user instanceof \WP_User ) { update_user_meta( $user->ID, self::PW_SET_META, time() ); } } public function stamp_on_update( $user_id ) { // profile_update fires after WP has verified its own update nonce; // we only read whether the password field is non-empty. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['pass1'] ) ) { update_user_meta( $user_id, self::PW_SET_META, time() ); } } public function expiry_notice() { $days = (int) Helper::get_setting( 'password_expiry_days', 0 ); if ( $days <= 0 || ! current_user_can( 'read' ) ) { return; } $set = (int) get_user_meta( get_current_user_id(), self::PW_SET_META, true ); if ( ! $set ) { // Unknown — stamp now so the clock starts. update_user_meta( get_current_user_id(), self::PW_SET_META, time() ); return; } if ( ( time() - $set ) > $days * DAY_IN_SECONDS ) { printf( '
%s %s